2025-08-07 06:09:37 -04:00
|
|
|
|
#!/usr/bin/env python3
|
2026-03-11 23:15:07 -04:00
|
|
|
|
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
2025-10-31 01:10:54 -04:00
|
|
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
2025-04-26 17:12:38 -04:00
|
|
|
|
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
import ast
|
2025-12-22 04:02:44 -05:00
|
|
|
|
import configparser
|
2025-08-07 06:09:37 -04:00
|
|
|
|
import datetime
|
[IMP] todo claude configs: list missing and outdated commands, redeploy
The command list showed only what sat in ~/.claude/commands, with a date:
a command never installed stayed invisible, and a copy older than its
template in conf/ looked the same as a current one. Each ERPLibre template is
now compared with its copy, ignoring the git identity line: not installed,
up to date, or to redeploy with its +/- line count; other commands are
marked not from ERPLibre. On y/N it shows the diff, then redeploys the
outdated copies, keeping the name and e-mail /commit carried.
Checked: 7 new tests on a temporary directory; test_todo, 81 tests, passes.
--- FR ---
[IMP] todo configs claude : commandes absentes, périmées, redéploiement
La liste des commandes ne montrait que le contenu de ~/.claude/commands,
avec une date : une commande jamais installée restait invisible, et une copie
plus vieille que son gabarit de conf/ ressemblait à une copie à jour. Chaque
gabarit d'ERPLibre est désormais comparé à sa copie, hors ligne d'identité
git : non installée, à jour, ou à redéployer avec son compte +/- ; les autres
commandes sont marquées hors ERPLibre. Sur o/N elle montre le diff, puis
redéploie les copies périmées en gardant le nom et le courriel de /commit.
Vérifié : 7 nouveaux tests sur un dossier temporaire ; test_todo, 81 tests.
Assisted-by: Claude Opus 5.5
2026-09-25 01:38:25 -04:00
|
|
|
|
import difflib
|
[FIX] démarrage : choisir un interpréteur capable de lire TODO
install.sh et make lançaient todo.py par son hashbang, donc par le python3 du
PATH, sans rien garantir de sa version : une syntaxe plus récente l'arrête en
SyntaxError au chargement, et le poste hors d'âge perd tout moyen de lancer
l'installation qui l'en sortirait. L'ordre est désormais le venv d'outillage,
sinon le python3 du PATH s'il suffit, sinon l'installation. TODO lui-même se
relance dans le venv, vérifie version et modules avant ses imports, et propose
l'installation en terminal — ce que « make » sur un clone neuf promet depuis
toujours sans le tenir.
Vérifié : venv à jour, venv périmé, systèmes en 3.10, 3.14 et 3.15 vont chacun
où il faut ; un import du module ne relance rien et lève comme avant.
--- EN ---
install.sh and make ran todo.py through its hashbang, so through the python3 of
the PATH, with no guarantee about its version: a newer syntax stops it with a
SyntaxError at load, and the ageing machine loses every way to start the
install that would rescue it. The order is now the tools venv, else the python3
of the PATH when it suffices, else the install. TODO itself relaunches in the
venv, checks version and modules before its imports, and offers the install in
a terminal — what "make" on a fresh clone has always promised without
keeping.
Checked: fresh venv, stale venv, systems on 3.10, 3.14 and 3.15 each go where
they should; importing the module relaunches nothing and raises as before.
Assisted-by: Claude Opus 5
2026-09-24 13:32:16 -04:00
|
|
|
|
import importlib.util
|
2026-08-07 03:22:26 -04:00
|
|
|
|
import inspect
|
2025-04-26 17:12:38 -04:00
|
|
|
|
import json
|
|
|
|
|
|
import logging
|
|
|
|
|
|
import os
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
import re
|
|
|
|
|
|
import shlex
|
2025-08-07 06:09:37 -04:00
|
|
|
|
import shutil
|
2026-08-23 04:17:34 -04:00
|
|
|
|
import socket
|
2025-08-07 06:09:37 -04:00
|
|
|
|
import subprocess
|
2025-05-04 00:40:05 -04:00
|
|
|
|
import sys
|
|
|
|
|
|
import time
|
2025-12-27 05:23:48 -05:00
|
|
|
|
import xml.etree.ElementTree as ET
|
2025-10-31 01:10:54 -04:00
|
|
|
|
import zipfile
|
|
|
|
|
|
|
|
|
|
|
|
new_path = os.path.normpath(
|
|
|
|
|
|
os.path.join(os.path.dirname(__file__), "..", "..")
|
|
|
|
|
|
)
|
|
|
|
|
|
sys.path.append(new_path)
|
|
|
|
|
|
|
[FIX] démarrage : choisir un interpréteur capable de lire TODO
install.sh et make lançaient todo.py par son hashbang, donc par le python3 du
PATH, sans rien garantir de sa version : une syntaxe plus récente l'arrête en
SyntaxError au chargement, et le poste hors d'âge perd tout moyen de lancer
l'installation qui l'en sortirait. L'ordre est désormais le venv d'outillage,
sinon le python3 du PATH s'il suffit, sinon l'installation. TODO lui-même se
relance dans le venv, vérifie version et modules avant ses imports, et propose
l'installation en terminal — ce que « make » sur un clone neuf promet depuis
toujours sans le tenir.
Vérifié : venv à jour, venv périmé, systèmes en 3.10, 3.14 et 3.15 vont chacun
où il faut ; un import du module ne relance rien et lève comme avant.
--- EN ---
install.sh and make ran todo.py through its hashbang, so through the python3 of
the PATH, with no guarantee about its version: a newer syntax stops it with a
SyntaxError at load, and the ageing machine loses every way to start the
install that would rescue it. The order is now the tools venv, else the python3
of the PATH when it suffices, else the install. TODO itself relaunches in the
venv, checks version and modules before its imports, and offers the install in
a terminal — what "make" on a fresh clone has always promised without
keeping.
Checked: fresh venv, stale venv, systems on 3.10, 3.14 and 3.15 each go where
they should; importing the module relaunches nothing and raises as before.
Assisted-by: Claude Opus 5
2026-09-24 13:32:16 -04:00
|
|
|
|
|
|
|
|
|
|
# Garde d'amorçage, joué seulement quand todo.py est le programme lancé : il
|
|
|
|
|
|
# relance dans le venv d'outillage, ou refuse avec le geste qui le bâtit, au
|
|
|
|
|
|
# lieu d'une trace brute à l'import. Il parse sous 3.7 pour parler à un vieux
|
|
|
|
|
|
# python. Messages en anglais hors t() : todo_i18n n'est pas encore importé.
|
|
|
|
|
|
RELAUNCH_ENV = "EL_TODO_VENV_RELAUNCHED"
|
|
|
|
|
|
# Modules tiers que charge l'import de ce fichier et de ses mixins : un seul
|
|
|
|
|
|
# absent fait lever l'import, ou boucler crash_diagnostic. Ceux qu'un menu
|
|
|
|
|
|
# n'importe qu'à l'usage n'y figurent pas, ils ne bloquent pas le démarrage.
|
|
|
|
|
|
REQUIRED_MODULES = (
|
|
|
|
|
|
"click colorama dotenv humanize openai pykeepass urwid".split()
|
|
|
|
|
|
)
|
|
|
|
|
|
INSTALL_CMD = "./script/install/install_erplibre.sh"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _read_conf(name, fallback):
|
|
|
|
|
|
"""Rend la première ligne non vide et non commentée de conf/<name>."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(os.path.join(new_path, "conf", name), encoding="utf-8") as f:
|
|
|
|
|
|
for line in f:
|
|
|
|
|
|
line = line.strip()
|
|
|
|
|
|
if line and not line.startswith("#"):
|
|
|
|
|
|
return line
|
|
|
|
|
|
except (OSError, UnicodeDecodeError):
|
|
|
|
|
|
pass
|
|
|
|
|
|
sys.stderr.write("conf/%s unreadable, assuming %s\n" % (name, fallback))
|
|
|
|
|
|
return fallback
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
VENV_ERPLIBRE = _read_conf("python-erplibre-venv", ".venv.erplibre")
|
|
|
|
|
|
VENV_DIR = os.path.join(new_path, VENV_ERPLIBRE)
|
|
|
|
|
|
VENV_PYTHON = os.path.join(VENV_DIR, "bin", "python")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _in_venv():
|
|
|
|
|
|
# sys.prefix et non le binaire : <venv>/bin/python est un lien vers sa base.
|
|
|
|
|
|
return os.path.realpath(sys.prefix) == os.path.realpath(VENV_DIR)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _relaunch(marker):
|
|
|
|
|
|
"""Remplace le processus par le python du venv ; OSError si impossible."""
|
|
|
|
|
|
env = dict(os.environ)
|
|
|
|
|
|
env[RELAUNCH_ENV] = marker
|
|
|
|
|
|
argv = [VENV_PYTHON, os.path.abspath(__file__)] + sys.argv[1:]
|
|
|
|
|
|
os.execve(VENV_PYTHON, argv, env)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _bootstrap():
|
|
|
|
|
|
# La relance passe avant la version : un python trop vieux avec un venv
|
|
|
|
|
|
# sain doit relancer, pas refuser.
|
|
|
|
|
|
relaunched = os.environ.get(RELAUNCH_ENV)
|
|
|
|
|
|
if not relaunched and not _in_venv() and os.access(VENV_PYTHON, os.X_OK):
|
|
|
|
|
|
try:
|
|
|
|
|
|
_relaunch("1")
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
voulue = _read_conf("python-erplibre-version", "3.14")
|
|
|
|
|
|
missing = [m for m in REQUIRED_MODULES if not importlib.util.find_spec(m)]
|
|
|
|
|
|
if sys.version_info[:2] >= tuple(map(int, voulue.split(".")[:2])):
|
|
|
|
|
|
if not missing:
|
|
|
|
|
|
os.environ.pop(RELAUNCH_ENV, None)
|
|
|
|
|
|
return
|
|
|
|
|
|
reason = "missing modules: %s" % ", ".join(missing)
|
|
|
|
|
|
else:
|
|
|
|
|
|
reason = "Python %s is older than %s" % (
|
|
|
|
|
|
sys.version.split()[0],
|
|
|
|
|
|
voulue,
|
|
|
|
|
|
)
|
|
|
|
|
|
print("TODO cannot start from %s: %s." % (sys.executable, reason))
|
|
|
|
|
|
print("Install the tools virtualenv %s with:" % VENV_ERPLIBRE)
|
|
|
|
|
|
print("\n %s\n" % INSTALL_CMD)
|
|
|
|
|
|
# Amorçage de « make » sur un clone neuf : proposer l'installation, une fois.
|
|
|
|
|
|
if relaunched != "installed" and os.isatty(0) and os.isatty(1):
|
|
|
|
|
|
try:
|
|
|
|
|
|
answer = input("Run it now? [y/o/N] ")
|
|
|
|
|
|
except (EOFError, KeyboardInterrupt):
|
|
|
|
|
|
answer = ""
|
|
|
|
|
|
if answer.strip().lower() in ("y", "yes", "o", "oui"):
|
|
|
|
|
|
if subprocess.call([INSTALL_CMD], cwd=new_path) != 0:
|
|
|
|
|
|
print("Installation failed, see above.")
|
|
|
|
|
|
else:
|
|
|
|
|
|
try:
|
|
|
|
|
|
_relaunch("installed")
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
print("Installed, but %s does not start." % VENV_PYTHON)
|
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
_bootstrap()
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-10-31 01:10:54 -04:00
|
|
|
|
from script.config import config_file
|
2026-02-19 12:27:35 -05:00
|
|
|
|
from script.execute import execute
|
[FIX] ssh config : une seule décision sur ce qu'est un nom de machine
Trois lecteurs de ~/.ssh/config tranchaient la même question autrement : l'un
comparait le mot-clé avec la casse, un autre exigeait un espace là où une
tabulation est légale, un troisième laissait passer le motif nié `!nom`. Un
alias déclaré « host » en minuscules était donc vu par l'un et invisible aux
deux autres, ce qui se lit comme une panne intermittente de la découverte.
La décision passe dans un module qui ne lit aucun fichier. Il rend None sur ce
qui n'est pas une déclaration et une liste — possiblement vide — sur ce qui en
est une : « Host * » ne nomme aucune machine mais reste une déclaration, sinon
ses directives se rattachent au bloc précédent. Vérifié : 19 tests, et le menu
reconnaît toujours le serveur du réseau par sa source SSH.
--- EN ---
Three readers of ~/.ssh/config settled the same question differently: one
compared the keyword case-sensitively, another required a space where a tab is
legal, a third let the negated pattern `!name` through. An alias declared with
a lowercase "host" was therefore seen by one and invisible to the other two,
which reads as intermittent discovery failure.
The decision moves into a module that reads no file. It returns None for what
is not a declaration and a list — possibly empty — for what is: "Host *" names
no machine yet remains a declaration, otherwise its directives attach to the
previous block. Checked: 19 tests, and the menu still recognises the server on
the network through its SSH source.
Assisted-by: Claude Opus 5
2026-09-09 04:39:01 -04:00
|
|
|
|
from script.todo import dev_tools, ssh_config, todo_install, todo_prefs
|
[ADD] assistant : serveur LLM reconnu, catalogue gpt, sessions locales
L'entrée « Question IA » envoyait toute question à une seule API distante, sur
un modèle figé et sans historique : aucun serveur local n'était atteignable,
et rien ne disait où partait le texte.
Deux règles gouvernent ce qui la remplace. Un port ne dit jamais qui répond —
un seul en héberge jusqu'à trois, et une famille réémet l'API d'une autre en
entier — donc l'identité se lit dans le CORPS, par une échelle ordonnée. Et
une adresse ne devient jamais du texte d'invite : le détecteur du dépôt ne
voit pas les noms d'hôte, d'où une poignée opaque à sa place.
Vérifié : 284 tests, et contre un serveur du réseau — reconnaissance,
capacités lues, conversation multi-tours, triage juste.
--- EN ---
The "AI question" entry sent every question to a single remote API, on a
frozen model and with no history: no local server was reachable, and nothing
said where the text was going.
Two rules govern what replaces it. A port never says who answers — one hosts
up to three products, and one family re-serves another's native API in full —
so identity is read from the BODY, by an ordered ladder. And an address never
becomes prompt text: the repository's detector does not see host names, hence
an opaque handle in its place.
Checked: 284 tests, and against a server on the network — recognition,
capabilities read, a multi-turn conversation, a correct triage.
Assisted-by: Claude Opus 5
2026-09-09 06:41:56 -04:00
|
|
|
|
from script.todo.assistant_menu import AssistantMenuMixin
|
2026-03-10 03:59:35 -04:00
|
|
|
|
from script.todo.database_manager import DatabaseManager
|
[ADD] qemu menu : récupérer des fichiers dans le disque d'une VM
Une VM qui ne démarre plus garde ses fichiers : libguestfs monte son
qcow2 sans elle. Toute commande porte « --ro », et c'est ce qui change la
manœuvre : ouvrir en écriture le disque d'une machine allumée corrompt
son système de fichiers, la lire ne risque rien. Un arrêt reste proposé
et non imposé — une lecture vivante voit un état peut-être incohérent,
un fichier à moitié écrit, un journal non rejoué. Le nom du paquet suit
la distribution, celui de Debian n'existant nulle part ailleurs.
Vérifié : 16 tests, rougis par quatre mutations — perdre le « --ro »,
donner le nom Debian partout, dupliquer un numéro, retirer l'entrée.
--- EN ---
A VM that no longer boots still holds its files: libguestfs mounts its
qcow2 without it. Every command carries « --ro », and that is what
changes the operation: opening a running machine's disk for writing
corrupts its filesystem, reading it risks nothing. A shutdown stays
offered, not imposed — a live read sees a possibly torn state, a
half-written file, an unreplayed journal. The package name follows the
distribution, Debian's existing nowhere else.
Checked: 16 tests, turned red by four mutations — losing « --ro », using
Debian's name everywhere, duplicating a number, dropping the entry.
Assisted-by: Claude Opus 5
2026-09-03 01:23:34 -04:00
|
|
|
|
from script.todo.kdbx_manager import KdbxManager
|
[REF] long_test : moteur commun, sûreté déclarée, sixième étape
deep_proxmox.py passe de 1245 à 474 lignes : tout ce qui ne connaît ni « qm »
ni pmxcfs vit désormais dans descente.py, prêt pour un second test long.
L'extraction a mis à nu ce qui protégeait un hôte qu'on n'a pas créé : rien.
a_defaire exigeait « vmid » et « parent_alias », deux clés que seule une
descente écrit — la protection tenait parce qu'aucun champ ne décrivait un
hôte emprunté. Un champ « cree », écrit à l'instant de la création, la rend
explicite et ferme trois portes : la liste de destruction, le repli par NOM de
detruire_etage1, et le retrait des entrées ~/.ssh/config de l'utilisateur.
Quatrième porte : le dossier des rapports est partagé. « deep_qemu --detruire »
aurait pris le rapport le plus récent, fût-il celui d'une descente Proxmox. Le
rapport porte son outil ; un rapport plus ancien, qui n'en a pas, est placé par
le préfixe de son nom de fichier plutôt que d'être rendu indéfaisable.
detruire_etage1 ne devine plus le nom de sa cible : il est obligatoire. Et une
sixième étape est née — « cet étage peut-il héberger le suivant ? » — parce que
le contrôle du stockage était celui du DÉBUT de l'étage suivant.
64 tests, les cinq garde-fous morts sous mutation. Au passage : la classe
LongTestMenuMixin, que mon renommage de répertoire avait rebaptisée
long_testMenuMixin sans qu'aucun test le voie.
--- EN ---
deep_proxmox.py drops from 1245 to 474 lines: everything that knows neither
"qm" nor pmxcfs now lives in descente.py, ready for a second long test.
The extraction laid bare what protected a host we did not create: nothing.
a_defaire required "vmid" and "parent_alias", two keys only a descent writes —
the protection held because no field described a borrowed host. A "cree" field,
written the instant a machine is created, makes it explicit and closes three
doors: the destroy list, detruire_etage1's fallback to the NAME, and the
removal of the user's own ~/.ssh/config entries.
Fourth door: the report directory is shared. "deep_qemu --detruire" would have
taken the most recent report, Proxmox's included. Reports now carry their tool;
an older one without it is placed by its filename prefix rather than made
undestroyable.
detruire_etage1 no longer guesses its target's name: it is mandatory. And a
sixth step is born — "can this level host the next?" — because the storage
check was the one at the START of the next level.
64 tests, all five guards die under mutation. Along the way: the class
LongTestMenuMixin, which my directory rename had turned into
long_testMenuMixin without any test noticing.
Assisted-by: claude-opus-5
(cherry picked from commit e1bc9ae3cfacd36a502bc88dc0789a4e86ce986b)
2026-08-28 01:31:36 -04:00
|
|
|
|
from script.todo.longtest_menu import LongTestMenuMixin
|
[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.proxmox_menu import ProxmoxMenuMixin
|
|
|
|
|
|
from script.todo.qemu_access import QemuAccessMixin
|
[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
|
|
|
|
from script.todo.qemu_cache_menu import QemuCacheMenuMixin
|
[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.qemu_deploy import QemuDeployMixin
|
|
|
|
|
|
from script.todo.qemu_install import QemuInstallMixin
|
|
|
|
|
|
from script.todo.qemu_manage import QemuManageMixin
|
|
|
|
|
|
from script.todo.qemu_menu import QemuMenuMixin
|
[ADD] qemu réseau : voir et recréer le sous-réseau des VM
Une VM prend son adresse dans le /24 de son réseau et en sort par le .1,
porté par le pont : déplacer ce /24 sous une VM allumée lui laisse un bail
qui ne mène nulle part, et abattre le réseau détache son tap, que libvirt
n'y remet pas. network_qemu.py tient les trois gestes dans l'ordre — arrêter
les VM attachées, redéfinir le réseau sur le préfixe voulu, rallumer celles
qu'il a arrêtées, elles seules. Une VM sourde au shutdown annule la
redéfinition plutôt que d'y perdre son pont ; --force-off coupe le courant.
Un préfixe qui recouvre ce que l'hôte route déjà est refusé.
Vérifié : 13 tests neufs, la numérotation du menu, une passe à blanc.
--- EN ---
A VM takes its address in its network's /24 and leaves through the .1,
carried by the bridge: moving that /24 under a live VM leaves it a lease
leading nowhere, and tearing the network down detaches its tap, which
libvirt does not put back. network_qemu.py holds the three steps in order —
shut down the attached VMs, redefine the network on the wanted prefix, start
again the ones it stopped, those only. A VM deaf to the shutdown cancels the
redefinition rather than losing its bridge; --force-off cuts the power.
A prefix overlapping what the host already routes is refused.
Checked: 13 new tests, the menu numbering, one dry run.
Assisted-by: Claude Opus 5
2026-09-04 03:31:22 -04:00
|
|
|
|
from script.todo.qemu_network import QemuNetworkMixin
|
[ADD] qemu menu : récupérer des fichiers dans le disque d'une VM
Une VM qui ne démarre plus garde ses fichiers : libguestfs monte son
qcow2 sans elle. Toute commande porte « --ro », et c'est ce qui change la
manœuvre : ouvrir en écriture le disque d'une machine allumée corrompt
son système de fichiers, la lire ne risque rien. Un arrêt reste proposé
et non imposé — une lecture vivante voit un état peut-être incohérent,
un fichier à moitié écrit, un journal non rejoué. Le nom du paquet suit
la distribution, celui de Debian n'existant nulle part ailleurs.
Vérifié : 16 tests, rougis par quatre mutations — perdre le « --ro »,
donner le nom Debian partout, dupliquer un numéro, retirer l'entrée.
--- EN ---
A VM that no longer boots still holds its files: libguestfs mounts its
qcow2 without it. Every command carries « --ro », and that is what
changes the operation: opening a running machine's disk for writing
corrupts its filesystem, reading it risks nothing. A shutdown stays
offered, not imposed — a live read sees a possibly torn state, a
half-written file, an unreplayed journal. The package name follows the
distribution, Debian's existing nowhere else.
Checked: 16 tests, turned red by four mutations — losing « --ro », using
Debian's name everywhere, duplicating a number, dropping the entry.
Assisted-by: Claude Opus 5
2026-09-03 01:23:34 -04:00
|
|
|
|
from script.todo.qemu_recover import QemuRecoverMixin
|
2026-08-07 03:22:26 -04:00
|
|
|
|
from script.todo.todo_i18n import get_lang, lang_is_configured, set_lang, t
|
[ADD] transform data : anonymiser un fichier externe ou une base Odoo
Partager un fichier ou une base demandait d'anonymiser à la main,
colonne par colonne, ou de ne pas partager. L'entrée ouvre un fichier
externe — Excel, Access, CSV, XML, JSON — ou une base Odoo, dit ce
qu'elle porte, puis en tire une copie. L'original n'est jamais touché.
Deux règles gouvernent ce qui sort. Une colonne n'est laissée intacte
que sur son CONTENU mesuré, hors onze étiquettes structurelles : décider
sur l'étiquette recopiait les colonnes les plus identifiantes en les
annonçant protégées. Et la copie est relue octet par octet, un classeur
portant de la donnée en une douzaine d'endroits hors des cellules.
Vérifié : 5 748 tests — 13 rouges, déjà rouges sur master.
--- EN ---
Sharing a file or a database meant anonymising by hand, column by
column, or not sharing. The entry opens an external file — Excel,
Access, CSV, XML, JSON — or an Odoo database, says what it holds, then
draws a copy of it. The original is never touched.
Two rules govern what comes out. A column is left alone on its measured
CONTENT, bar eleven structural labels: deciding on the label copied the
most identifying columns verbatim while announcing them as protected.
And the copy is reread byte by byte, a workbook holding data in a dozen
places that are not cells.
Checked: 5,748 tests — 13 red, already red on master.
Assisted-by: Claude Opus 5
2026-09-10 23:58:01 -04:00
|
|
|
|
from script.todo.transform_menu import TransformMenuMixin
|
2026-03-10 03:59:35 -04:00
|
|
|
|
from script.todo.version_manager import get_odoo_version
|
[ADD] assistant : serveur LLM reconnu, catalogue gpt, sessions locales
L'entrée « Question IA » envoyait toute question à une seule API distante, sur
un modèle figé et sans historique : aucun serveur local n'était atteignable,
et rien ne disait où partait le texte.
Deux règles gouvernent ce qui la remplace. Un port ne dit jamais qui répond —
un seul en héberge jusqu'à trois, et une famille réémet l'API d'une autre en
entier — donc l'identité se lit dans le CORPS, par une échelle ordonnée. Et
une adresse ne devient jamais du texte d'invite : le détecteur du dépôt ne
voit pas les noms d'hôte, d'où une poignée opaque à sa place.
Vérifié : 284 tests, et contre un serveur du réseau — reconnaissance,
capacités lues, conversation multi-tours, triage juste.
--- EN ---
The "AI question" entry sent every question to a single remote API, on a
frozen model and with no history: no local server was reachable, and nothing
said where the text was going.
Two rules govern what replaces it. A port never says who answers — one hosts
up to three products, and one family re-serves another's native API in full —
so identity is read from the BODY, by an ordered ladder. And an address never
becomes prompt text: the repository's detector does not see host names, hence
an opaque handle in its place.
Checked: 284 tests, and against a server on the network — recognition,
capabilities read, a multi-turn conversation, a correct triage.
Assisted-by: Claude Opus 5
2026-09-09 06:41:56 -04:00
|
|
|
|
from script.todo.vpn_menu import VpnMenuMixin
|
2025-08-07 06:09:37 -04:00
|
|
|
|
|
2026-03-10 03:06:07 -04:00
|
|
|
|
ERROR_LOG_PATH = ".erplibre.error.txt"
|
2025-08-07 06:09:37 -04:00
|
|
|
|
ENABLE_CRASH = False
|
|
|
|
|
|
CRASH_E = None
|
2025-12-27 05:23:48 -05:00
|
|
|
|
# Support mobile ERPLibre
|
|
|
|
|
|
ANDROID_DIR = "android"
|
2025-12-28 00:37:45 -05:00
|
|
|
|
MOBILE_HOME_PATH = "./mobile/erplibre_home_mobile"
|
2025-12-27 05:23:48 -05:00
|
|
|
|
STRINGS_FILE = os.path.join(
|
|
|
|
|
|
MOBILE_HOME_PATH, ANDROID_DIR, "app/src/main/res/values/strings.xml"
|
|
|
|
|
|
)
|
|
|
|
|
|
GRADLE_FILE = os.path.join(MOBILE_HOME_PATH, ANDROID_DIR, "app/build.gradle")
|
2025-04-26 17:12:38 -04:00
|
|
|
|
|
2026-02-14 05:53:31 -05:00
|
|
|
|
|
2025-05-04 00:40:05 -04:00
|
|
|
|
try:
|
|
|
|
|
|
import click
|
2025-10-01 04:53:08 -04:00
|
|
|
|
import dotenv
|
2025-08-07 06:09:37 -04:00
|
|
|
|
import humanize
|
2025-05-04 00:40:05 -04:00
|
|
|
|
import openai
|
2025-10-31 01:10:54 -04:00
|
|
|
|
import todo_file_browser
|
2025-08-07 06:09:37 -04:00
|
|
|
|
|
|
|
|
|
|
# import urwid
|
|
|
|
|
|
# TODO implement rich for beautiful print and table
|
|
|
|
|
|
# import rich
|
2025-10-31 01:10:54 -04:00
|
|
|
|
import todo_upgrade
|
|
|
|
|
|
from pykeepass import PyKeePass
|
2025-05-04 00:40:05 -04:00
|
|
|
|
except ModuleNotFoundError as e:
|
2025-08-07 06:09:37 -04:00
|
|
|
|
humanize = None
|
|
|
|
|
|
ENABLE_CRASH = True
|
|
|
|
|
|
CRASH_E = e
|
2025-05-04 01:02:12 -04:00
|
|
|
|
|
2025-08-07 06:09:37 -04:00
|
|
|
|
if not ENABLE_CRASH:
|
2026-03-07 02:59:07 -05:00
|
|
|
|
print(t("Importation success!"))
|
2025-04-26 17:12:38 -04:00
|
|
|
|
|
2025-10-31 01:10:54 -04:00
|
|
|
|
logging.basicConfig(
|
|
|
|
|
|
format=(
|
|
|
|
|
|
"%(asctime)s,%(msecs)d %(levelname)-8s [%(filename)s:%(lineno)d]"
|
|
|
|
|
|
" %(message)s"
|
|
|
|
|
|
),
|
|
|
|
|
|
datefmt="%Y-%m-%d:%H:%M:%S",
|
|
|
|
|
|
level=logging.INFO,
|
|
|
|
|
|
)
|
2025-04-26 17:12:38 -04:00
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
CONFIG_FILE = "./script/todo/todo.json"
|
2025-10-31 01:10:54 -04:00
|
|
|
|
CONFIG_OVERRIDE_FILE = "./private/todo/todo.json"
|
2025-04-26 17:12:38 -04:00
|
|
|
|
LOGO_ASCII_FILE = "./script/todo/logo_ascii.txt"
|
|
|
|
|
|
|
|
|
|
|
|
|
[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
|
|
|
|
class TODO(
|
|
|
|
|
|
# L'ordre est celui de la lecture, pas de la résolution : aucun nom n'est
|
|
|
|
|
|
# défini deux fois (une classe unique jusqu'ici), donc aucune priorité à
|
|
|
|
|
|
# arbitrer. Chaque fichier porte un sujet, et son en-tête dit sa frontière.
|
[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
|
|
|
|
QemuCacheMenuMixin,
|
[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
|
|
|
|
QemuMenuMixin,
|
|
|
|
|
|
QemuDeployMixin,
|
|
|
|
|
|
QemuInstallMixin,
|
|
|
|
|
|
QemuManageMixin,
|
[ADD] qemu réseau : voir et recréer le sous-réseau des VM
Une VM prend son adresse dans le /24 de son réseau et en sort par le .1,
porté par le pont : déplacer ce /24 sous une VM allumée lui laisse un bail
qui ne mène nulle part, et abattre le réseau détache son tap, que libvirt
n'y remet pas. network_qemu.py tient les trois gestes dans l'ordre — arrêter
les VM attachées, redéfinir le réseau sur le préfixe voulu, rallumer celles
qu'il a arrêtées, elles seules. Une VM sourde au shutdown annule la
redéfinition plutôt que d'y perdre son pont ; --force-off coupe le courant.
Un préfixe qui recouvre ce que l'hôte route déjà est refusé.
Vérifié : 13 tests neufs, la numérotation du menu, une passe à blanc.
--- EN ---
A VM takes its address in its network's /24 and leaves through the .1,
carried by the bridge: moving that /24 under a live VM leaves it a lease
leading nowhere, and tearing the network down detaches its tap, which
libvirt does not put back. network_qemu.py holds the three steps in order —
shut down the attached VMs, redefine the network on the wanted prefix, start
again the ones it stopped, those only. A VM deaf to the shutdown cancels the
redefinition rather than losing its bridge; --force-off cuts the power.
A prefix overlapping what the host already routes is refused.
Checked: 13 new tests, the menu numbering, one dry run.
Assisted-by: Claude Opus 5
2026-09-04 03:31:22 -04:00
|
|
|
|
QemuNetworkMixin,
|
[ADD] qemu menu : récupérer des fichiers dans le disque d'une VM
Une VM qui ne démarre plus garde ses fichiers : libguestfs monte son
qcow2 sans elle. Toute commande porte « --ro », et c'est ce qui change la
manœuvre : ouvrir en écriture le disque d'une machine allumée corrompt
son système de fichiers, la lire ne risque rien. Un arrêt reste proposé
et non imposé — une lecture vivante voit un état peut-être incohérent,
un fichier à moitié écrit, un journal non rejoué. Le nom du paquet suit
la distribution, celui de Debian n'existant nulle part ailleurs.
Vérifié : 16 tests, rougis par quatre mutations — perdre le « --ro »,
donner le nom Debian partout, dupliquer un numéro, retirer l'entrée.
--- EN ---
A VM that no longer boots still holds its files: libguestfs mounts its
qcow2 without it. Every command carries « --ro », and that is what
changes the operation: opening a running machine's disk for writing
corrupts its filesystem, reading it risks nothing. A shutdown stays
offered, not imposed — a live read sees a possibly torn state, a
half-written file, an unreplayed journal. The package name follows the
distribution, Debian's existing nowhere else.
Checked: 16 tests, turned red by four mutations — losing « --ro », using
Debian's name everywhere, duplicating a number, dropping the entry.
Assisted-by: Claude Opus 5
2026-09-03 01:23:34 -04:00
|
|
|
|
QemuRecoverMixin,
|
[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
|
|
|
|
QemuAccessMixin,
|
|
|
|
|
|
ProxmoxMenuMixin,
|
[REF] long_test : moteur commun, sûreté déclarée, sixième étape
deep_proxmox.py passe de 1245 à 474 lignes : tout ce qui ne connaît ni « qm »
ni pmxcfs vit désormais dans descente.py, prêt pour un second test long.
L'extraction a mis à nu ce qui protégeait un hôte qu'on n'a pas créé : rien.
a_defaire exigeait « vmid » et « parent_alias », deux clés que seule une
descente écrit — la protection tenait parce qu'aucun champ ne décrivait un
hôte emprunté. Un champ « cree », écrit à l'instant de la création, la rend
explicite et ferme trois portes : la liste de destruction, le repli par NOM de
detruire_etage1, et le retrait des entrées ~/.ssh/config de l'utilisateur.
Quatrième porte : le dossier des rapports est partagé. « deep_qemu --detruire »
aurait pris le rapport le plus récent, fût-il celui d'une descente Proxmox. Le
rapport porte son outil ; un rapport plus ancien, qui n'en a pas, est placé par
le préfixe de son nom de fichier plutôt que d'être rendu indéfaisable.
detruire_etage1 ne devine plus le nom de sa cible : il est obligatoire. Et une
sixième étape est née — « cet étage peut-il héberger le suivant ? » — parce que
le contrôle du stockage était celui du DÉBUT de l'étage suivant.
64 tests, les cinq garde-fous morts sous mutation. Au passage : la classe
LongTestMenuMixin, que mon renommage de répertoire avait rebaptisée
long_testMenuMixin sans qu'aucun test le voie.
--- EN ---
deep_proxmox.py drops from 1245 to 474 lines: everything that knows neither
"qm" nor pmxcfs now lives in descente.py, ready for a second long test.
The extraction laid bare what protected a host we did not create: nothing.
a_defaire required "vmid" and "parent_alias", two keys only a descent writes —
the protection held because no field described a borrowed host. A "cree" field,
written the instant a machine is created, makes it explicit and closes three
doors: the destroy list, detruire_etage1's fallback to the NAME, and the
removal of the user's own ~/.ssh/config entries.
Fourth door: the report directory is shared. "deep_qemu --detruire" would have
taken the most recent report, Proxmox's included. Reports now carry their tool;
an older one without it is placed by its filename prefix rather than made
undestroyable.
detruire_etage1 no longer guesses its target's name: it is mandatory. And a
sixth step is born — "can this level host the next?" — because the storage
check was the one at the START of the next level.
64 tests, all five guards die under mutation. Along the way: the class
LongTestMenuMixin, which my directory rename had turned into
long_testMenuMixin without any test noticing.
Assisted-by: claude-opus-5
(cherry picked from commit e1bc9ae3cfacd36a502bc88dc0789a4e86ce986b)
2026-08-28 01:31:36 -04:00
|
|
|
|
LongTestMenuMixin,
|
[ADD] transform data : anonymiser un fichier externe ou une base Odoo
Partager un fichier ou une base demandait d'anonymiser à la main,
colonne par colonne, ou de ne pas partager. L'entrée ouvre un fichier
externe — Excel, Access, CSV, XML, JSON — ou une base Odoo, dit ce
qu'elle porte, puis en tire une copie. L'original n'est jamais touché.
Deux règles gouvernent ce qui sort. Une colonne n'est laissée intacte
que sur son CONTENU mesuré, hors onze étiquettes structurelles : décider
sur l'étiquette recopiait les colonnes les plus identifiantes en les
annonçant protégées. Et la copie est relue octet par octet, un classeur
portant de la donnée en une douzaine d'endroits hors des cellules.
Vérifié : 5 748 tests — 13 rouges, déjà rouges sur master.
--- EN ---
Sharing a file or a database meant anonymising by hand, column by
column, or not sharing. The entry opens an external file — Excel,
Access, CSV, XML, JSON — or an Odoo database, says what it holds, then
draws a copy of it. The original is never touched.
Two rules govern what comes out. A column is left alone on its measured
CONTENT, bar eleven structural labels: deciding on the label copied the
most identifying columns verbatim while announcing them as protected.
And the copy is reread byte by byte, a workbook holding data in a dozen
places that are not cells.
Checked: 5,748 tests — 13 red, already red on master.
Assisted-by: Claude Opus 5
2026-09-10 23:58:01 -04:00
|
|
|
|
TransformMenuMixin,
|
[ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.
Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.
--- EN ---
`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.
The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.
Assisted-by: Claude Opus 5
2026-09-03 23:43:02 -04:00
|
|
|
|
VpnMenuMixin,
|
[ADD] assistant : serveur LLM reconnu, catalogue gpt, sessions locales
L'entrée « Question IA » envoyait toute question à une seule API distante, sur
un modèle figé et sans historique : aucun serveur local n'était atteignable,
et rien ne disait où partait le texte.
Deux règles gouvernent ce qui la remplace. Un port ne dit jamais qui répond —
un seul en héberge jusqu'à trois, et une famille réémet l'API d'une autre en
entier — donc l'identité se lit dans le CORPS, par une échelle ordonnée. Et
une adresse ne devient jamais du texte d'invite : le détecteur du dépôt ne
voit pas les noms d'hôte, d'où une poignée opaque à sa place.
Vérifié : 284 tests, et contre un serveur du réseau — reconnaissance,
capacités lues, conversation multi-tours, triage juste.
--- EN ---
The "AI question" entry sent every question to a single remote API, on a
frozen model and with no history: no local server was reachable, and nothing
said where the text was going.
Two rules govern what replaces it. A port never says who answers — one hosts
up to three products, and one family re-serves another's native API in full —
so identity is read from the BODY, by an ordered ladder. And an address never
becomes prompt text: the repository's detector does not see host names, hence
an opaque handle in its place.
Checked: 284 tests, and against a server on the network — recognition,
capabilities read, a multi-turn conversation, a correct triage.
Assisted-by: Claude Opus 5
2026-09-09 06:41:56 -04:00
|
|
|
|
AssistantMenuMixin,
|
[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
|
|
|
|
):
|
2025-04-26 17:12:38 -04:00
|
|
|
|
def __init__(self):
|
2025-10-31 01:10:54 -04:00
|
|
|
|
self.dir_path = None
|
2026-03-10 03:45:11 -04:00
|
|
|
|
self.selected_file_path = None
|
2025-10-31 01:10:54 -04:00
|
|
|
|
self.config_file = config_file.ConfigFile()
|
2026-02-13 01:27:38 -05:00
|
|
|
|
self.execute = execute.Execute()
|
2026-03-10 03:59:35 -04:00
|
|
|
|
self.kdbx_manager = KdbxManager(self.config_file)
|
2026-03-10 04:05:04 -04:00
|
|
|
|
self.db_manager = DatabaseManager(self.execute, self.fill_help_info)
|
2025-04-26 17:12:38 -04:00
|
|
|
|
|
2026-03-07 02:59:07 -05:00
|
|
|
|
def _ask_language(self):
|
|
|
|
|
|
if not lang_is_configured():
|
|
|
|
|
|
print()
|
2026-09-25 01:41:45 -04:00
|
|
|
|
print("🌍 Choisir la langue / Choose language:")
|
|
|
|
|
|
print("[1] 🇫🇷 Français")
|
|
|
|
|
|
print("[2] 🇬🇧 English")
|
2026-03-07 02:59:07 -05:00
|
|
|
|
choice = ""
|
|
|
|
|
|
while choice not in ("1", "2"):
|
|
|
|
|
|
choice = input("Select / Choisir : ").strip()
|
|
|
|
|
|
if choice == "1":
|
|
|
|
|
|
set_lang("fr")
|
|
|
|
|
|
else:
|
|
|
|
|
|
set_lang("en")
|
|
|
|
|
|
|
|
|
|
|
|
def _change_language(self):
|
|
|
|
|
|
print()
|
2026-09-25 01:41:45 -04:00
|
|
|
|
print("🌍 " + t("Choose language / Choisir la langue") + ":")
|
|
|
|
|
|
print(f"[1] 🇫🇷 {t('French')}")
|
|
|
|
|
|
print(f"[2] 🇬🇧 {t('English')}")
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(f"[0] {t('Back')}")
|
2026-03-07 02:59:07 -05:00
|
|
|
|
choice = ""
|
|
|
|
|
|
while choice not in ("0", "1", "2"):
|
2026-03-12 05:31:45 -04:00
|
|
|
|
choice = input(t("Select: ")).strip()
|
2026-03-07 02:59:07 -05:00
|
|
|
|
if choice == "0":
|
|
|
|
|
|
return False
|
|
|
|
|
|
elif choice == "1":
|
|
|
|
|
|
set_lang("fr")
|
|
|
|
|
|
else:
|
|
|
|
|
|
set_lang("en")
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(t("Language changed to: English"))
|
2026-03-07 02:59:07 -05:00
|
|
|
|
|
2026-08-10 03:10:50 -04:00
|
|
|
|
def run(self):
|
|
|
|
|
|
with open(self.config_file.get_logo_ascii_file_path()) as my_file:
|
|
|
|
|
|
print(my_file.read())
|
|
|
|
|
|
self._ask_language()
|
|
|
|
|
|
print(t("Opening TODO ..."))
|
|
|
|
|
|
print(f"🤖 {t('=> Enter your choice by number and press Enter!')}")
|
|
|
|
|
|
help_info = f"""{self._menu_header()}
|
|
|
|
|
|
[1] {t("Execute")}
|
|
|
|
|
|
[2] {t("Install")}
|
|
|
|
|
|
[3] {t("Assistant")}
|
[IMP] todo menus: language only in Configuration, Fork moved there
Execute carried a Preferences section whose one entry, the language, is
already Configuration's first item: two places for one setting. It is
removed, with its two unused translations. Fork, which opens a second TODO,
leaves the main menu for Configuration; the main menu now numbers Telemetry
4 and Configuration 5, and Reset all preferences becomes 6.
Checked: Configuration displayed with Fork at 5; 108 tests pass.
--- FR ---
[IMP] menus todo : langue dans Configuration seule, Fork y déménage
Execute portait une section Préférences dont la seule entrée, la langue, est
déjà la première de Configuration : deux endroits pour un réglage. Elle part,
avec ses deux traductions devenues inutiles. Fork, qui ouvre un second TODO,
quitte le menu principal pour Configuration ; le menu principal numérote
désormais Télémétrie 4 et Configuration 5, et la réinitialisation devient 6.
Vérifié : Configuration affichée avec Fork en 5 ; 108 tests passent.
Assisted-by: Claude Opus 5.5
2026-09-25 01:41:48 -04:00
|
|
|
|
[4] {t("Navigation telemetry (TUI)")}
|
|
|
|
|
|
[5] {t("Configuration")}
|
[IMP] script todo : icônes de menu, alignées sur leur largeur rendue
Le menu Git avait posé la convention ; dix autres restaient nus, et une
liste sans icône se lit ligne à ligne. L'espacement suit la largeur
RENDUE, lue dans Unicode et non devinée : deux espaces derrière un emoji
d'une colonne, une derrière un large — d'où le changement de signe de la
Configuration, dont le sien décalait le menu principal. Un même concept
garde son signe d'un menu à l'autre. « Quitter » garde sa traduction nue,
sa clé servant onze fois en raccourci de pied de page : son icône se pose
sur la ligne du menu.
Vérifié : menus rendus dans les deux langues, aucune clé incomplète,
aucune icône divergente entre les langues.
--- EN ---
The Git menu had set the convention; ten others stayed bare, and a list
without icons is read line by line. Spacing follows the RENDERED width,
read from Unicode rather than guessed: two spaces after a one-column
emoji, one after a wide one — hence the new sign for Configuration, whose
own offset the main menu. One concept keeps its sign from menu to menu.
« Quit » keeps its bare translation, its key serving eleven times as a
footer shortcut: its icon sits on the menu's own line.
Checked: menus rendered in both languages, no incomplete key, no icon
diverging between languages.
Assisted-by: Claude Opus 5
2026-09-03 03:08:18 -04:00
|
|
|
|
[0] 🚪 {t("Quit")}
|
2026-08-10 03:10:50 -04:00
|
|
|
|
"""
|
|
|
|
|
|
while True:
|
|
|
|
|
|
try:
|
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
|
except NameError:
|
|
|
|
|
|
print("Do")
|
|
|
|
|
|
print(f"source ./{VENV_ERPLIBRE}/bin/activate && make")
|
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
except ImportError:
|
|
|
|
|
|
print("Do")
|
|
|
|
|
|
print(f"source ./{VENV_ERPLIBRE}/bin/activate && make")
|
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
except click.exceptions.Abort:
|
|
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
print()
|
|
|
|
|
|
if status == "0":
|
|
|
|
|
|
break
|
|
|
|
|
|
elif status == "1":
|
|
|
|
|
|
self.prompt_execute()
|
|
|
|
|
|
elif status == "2":
|
|
|
|
|
|
self.prompt_install()
|
|
|
|
|
|
elif status == "3":
|
|
|
|
|
|
self.prompt_assistant()
|
|
|
|
|
|
elif status == "4":
|
|
|
|
|
|
self._todo_telemetry_tui()
|
[IMP] todo menus: language only in Configuration, Fork moved there
Execute carried a Preferences section whose one entry, the language, is
already Configuration's first item: two places for one setting. It is
removed, with its two unused translations. Fork, which opens a second TODO,
leaves the main menu for Configuration; the main menu now numbers Telemetry
4 and Configuration 5, and Reset all preferences becomes 6.
Checked: Configuration displayed with Fork at 5; 108 tests pass.
--- FR ---
[IMP] menus todo : langue dans Configuration seule, Fork y déménage
Execute portait une section Préférences dont la seule entrée, la langue, est
déjà la première de Configuration : deux endroits pour un réglage. Elle part,
avec ses deux traductions devenues inutiles. Fork, qui ouvre un second TODO,
quitte le menu principal pour Configuration ; le menu principal numérote
désormais Télémétrie 4 et Configuration 5, et la réinitialisation devient 6.
Vérifié : Configuration affichée avec Fork en 5 ; 108 tests passent.
Assisted-by: Claude Opus 5.5
2026-09-25 01:41:48 -04:00
|
|
|
|
elif status == "5":
|
2026-08-10 03:10:50 -04:00
|
|
|
|
self.prompt_configuration()
|
|
|
|
|
|
# elif status == "3" or status == "install":
|
|
|
|
|
|
# print("install")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(t("Command not found !"))
|
|
|
|
|
|
|
|
|
|
|
|
print(status)
|
|
|
|
|
|
# manipuler()
|
|
|
|
|
|
|
|
|
|
|
|
def prompt_assistant(self):
|
|
|
|
|
|
"""Ce qui s'adresse à l'humain : poser une question, lire son courriel."""
|
|
|
|
|
|
from script.todo.mail.menu import prompt_execute_mail
|
|
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
help_info = f"""{self._menu_header()}
|
[ADD] assistant : serveur LLM reconnu, catalogue gpt, sessions locales
L'entrée « Question IA » envoyait toute question à une seule API distante, sur
un modèle figé et sans historique : aucun serveur local n'était atteignable,
et rien ne disait où partait le texte.
Deux règles gouvernent ce qui la remplace. Un port ne dit jamais qui répond —
un seul en héberge jusqu'à trois, et une famille réémet l'API d'une autre en
entier — donc l'identité se lit dans le CORPS, par une échelle ordonnée. Et
une adresse ne devient jamais du texte d'invite : le détecteur du dépôt ne
voit pas les noms d'hôte, d'où une poignée opaque à sa place.
Vérifié : 284 tests, et contre un serveur du réseau — reconnaissance,
capacités lues, conversation multi-tours, triage juste.
--- EN ---
The "AI question" entry sent every question to a single remote API, on a
frozen model and with no history: no local server was reachable, and nothing
said where the text was going.
Two rules govern what replaces it. A port never says who answers — one hosts
up to three products, and one family re-serves another's native API in full —
so identity is read from the BODY, by an ordered ladder. And an address never
becomes prompt text: the repository's detector does not see host names, hence
an opaque handle in its place.
Checked: 284 tests, and against a server on the network — recognition,
capabilities read, a multi-turn conversation, a correct triage.
Assisted-by: Claude Opus 5
2026-09-09 06:41:56 -04:00
|
|
|
|
[1] {t("AI question - Ask a model, local or remote")}
|
2026-08-10 03:10:50 -04:00
|
|
|
|
[2] {t("mail_menu")}
|
|
|
|
|
|
[0] {t("Back")}"""
|
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
|
print()
|
|
|
|
|
|
if status == "0":
|
|
|
|
|
|
return
|
|
|
|
|
|
if status == "1":
|
[ADD] assistant : serveur LLM reconnu, catalogue gpt, sessions locales
L'entrée « Question IA » envoyait toute question à une seule API distante, sur
un modèle figé et sans historique : aucun serveur local n'était atteignable,
et rien ne disait où partait le texte.
Deux règles gouvernent ce qui la remplace. Un port ne dit jamais qui répond —
un seul en héberge jusqu'à trois, et une famille réémet l'API d'une autre en
entier — donc l'identité se lit dans le CORPS, par une échelle ordonnée. Et
une adresse ne devient jamais du texte d'invite : le détecteur du dépôt ne
voit pas les noms d'hôte, d'où une poignée opaque à sa place.
Vérifié : 284 tests, et contre un serveur du réseau — reconnaissance,
capacités lues, conversation multi-tours, triage juste.
--- EN ---
The "AI question" entry sent every question to a single remote API, on a
frozen model and with no history: no local server was reachable, and nothing
said where the text was going.
Two rules govern what replaces it. A port never says who answers — one hosts
up to three products, and one family re-serves another's native API in full —
so identity is read from the BODY, by an ordered ladder. And an address never
becomes prompt text: the repository's detector does not see host names, hence
an opaque handle in its place.
Checked: 284 tests, and against a server on the network — recognition,
capabilities read, a multi-turn conversation, a correct triage.
Assisted-by: Claude Opus 5
2026-09-09 06:41:56 -04:00
|
|
|
|
self.prompt_assistant_llm()
|
2026-08-10 03:10:50 -04:00
|
|
|
|
elif status == "2":
|
|
|
|
|
|
prompt_execute_mail(self)
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(t("Command not found !"))
|
|
|
|
|
|
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
def prompt_execute(self):
|
|
|
|
|
|
help_info = f"""{self._menu_header()}
|
2025-04-26 17:12:38 -04:00
|
|
|
|
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
── {t("Development")} ──
|
|
|
|
|
|
[1] {t("Code - Developer tools")}
|
|
|
|
|
|
[2] {t("Config - Configuration file management")}
|
|
|
|
|
|
[3] {t("Run - Execute and install an instance")}
|
|
|
|
|
|
[4] {t("Test - Test an Odoo module")}
|
|
|
|
|
|
[5] {t("Process - Execution tools")}
|
2025-04-26 17:12:38 -04:00
|
|
|
|
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
── {t("Data")} ──
|
|
|
|
|
|
[6] {t("Database - Database tools")}
|
[ADD] analyse: inspect an Odoo database without restoring it
A read-only toolkit answering « what is in this database, and what will
break on a version bump »: schema size and tables no model claims,
customised views compared with their module source, and the x_ fields
Studio left behind. It reads a backup zip directly, so a 40 GB dump needs
no restore, and it reaches every answer from the TODO menu.
--- FR ---
Une boîte à outils en lecture seule qui répond à « qu'y a-t-il dans cette
base, et qu'est-ce qui cassera à la montée de version » : taille du schéma
et tables qu'aucun modèle ne réclame, vues personnalisées comparées à leur
source, champs x_ laissés par Studio. Elle lit un zip de sauvegarde tel
quel, donc sans restaurer, et tout s'atteint depuis le menu TODO.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
[7] {t("Analyse - Odoo database analysis")}
|
[ADD] transform data : anonymiser un fichier externe ou une base Odoo
Partager un fichier ou une base demandait d'anonymiser à la main,
colonne par colonne, ou de ne pas partager. L'entrée ouvre un fichier
externe — Excel, Access, CSV, XML, JSON — ou une base Odoo, dit ce
qu'elle porte, puis en tire une copie. L'original n'est jamais touché.
Deux règles gouvernent ce qui sort. Une colonne n'est laissée intacte
que sur son CONTENU mesuré, hors onze étiquettes structurelles : décider
sur l'étiquette recopiait les colonnes les plus identifiantes en les
annonçant protégées. Et la copie est relue octet par octet, un classeur
portant de la donnée en une douzaine d'endroits hors des cellules.
Vérifié : 5 748 tests — 13 rouges, déjà rouges sur master.
--- EN ---
Sharing a file or a database meant anonymising by hand, column by
column, or not sharing. The entry opens an external file — Excel,
Access, CSV, XML, JSON — or an Odoo database, says what it holds, then
draws a copy of it. The original is never touched.
Two rules govern what comes out. A column is left alone on its measured
CONTENT, bar eleven structural labels: deciding on the label copied the
most identifying columns verbatim while announcing them as protected.
And the copy is reread byte by byte, a workbook holding data in a dozen
places that are not cells.
Checked: 5,748 tests — 13 red, already red on master.
Assisted-by: Claude Opus 5
2026-09-10 23:58:01 -04:00
|
|
|
|
[8] {t("Transform data - Transform your data")}
|
2025-04-26 17:12:38 -04:00
|
|
|
|
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
── {t("Sources & documentation")} ──
|
[ADD] transform data : anonymiser un fichier externe ou une base Odoo
Partager un fichier ou une base demandait d'anonymiser à la main,
colonne par colonne, ou de ne pas partager. L'entrée ouvre un fichier
externe — Excel, Access, CSV, XML, JSON — ou une base Odoo, dit ce
qu'elle porte, puis en tire une copie. L'original n'est jamais touché.
Deux règles gouvernent ce qui sort. Une colonne n'est laissée intacte
que sur son CONTENU mesuré, hors onze étiquettes structurelles : décider
sur l'étiquette recopiait les colonnes les plus identifiantes en les
annonçant protégées. Et la copie est relue octet par octet, un classeur
portant de la donnée en une douzaine d'endroits hors des cellules.
Vérifié : 5 748 tests — 13 rouges, déjà rouges sur master.
--- EN ---
Sharing a file or a database meant anonymising by hand, column by
column, or not sharing. The entry opens an external file — Excel,
Access, CSV, XML, JSON — or an Odoo database, says what it holds, then
draws a copy of it. The original is never touched.
Two rules govern what comes out. A column is left alone on its measured
CONTENT, bar eleven structural labels: deciding on the label copied the
most identifying columns verbatim while announcing them as protected.
And the copy is reread byte by byte, a workbook holding data in a dozen
places that are not cells.
Checked: 5,748 tests — 13 red, already red on master.
Assisted-by: Claude Opus 5
2026-09-10 23:58:01 -04:00
|
|
|
|
[9] {t("Git - Git and shell tools")}
|
|
|
|
|
|
[10] {t("Doc - Documentation search")}
|
2025-04-26 17:12:38 -04:00
|
|
|
|
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
── {t("AI & automation")} ──
|
[ADD] transform data : anonymiser un fichier externe ou une base Odoo
Partager un fichier ou une base demandait d'anonymiser à la main,
colonne par colonne, ou de ne pas partager. L'entrée ouvre un fichier
externe — Excel, Access, CSV, XML, JSON — ou une base Odoo, dit ce
qu'elle porte, puis en tire une copie. L'original n'est jamais touché.
Deux règles gouvernent ce qui sort. Une colonne n'est laissée intacte
que sur son CONTENU mesuré, hors onze étiquettes structurelles : décider
sur l'étiquette recopiait les colonnes les plus identifiantes en les
annonçant protégées. Et la copie est relue octet par octet, un classeur
portant de la donnée en une douzaine d'endroits hors des cellules.
Vérifié : 5 748 tests — 13 rouges, déjà rouges sur master.
--- EN ---
Sharing a file or a database meant anonymising by hand, column by
column, or not sharing. The entry opens an external file — Excel,
Access, CSV, XML, JSON — or an Odoo database, says what it holds, then
draws a copy of it. The original is never touched.
Two rules govern what comes out. A column is left alone on its measured
CONTENT, bar eleven structural labels: deciding on the label copied the
most identifying columns verbatim while announcing them as protected.
And the copy is reread byte by byte, a workbook holding data in a dozen
places that are not cells.
Checked: 5,748 tests — 13 red, already red on master.
Assisted-by: Claude Opus 5
2026-09-10 23:58:01 -04:00
|
|
|
|
[11] {t("GPT code - AI assistant tools")}
|
|
|
|
|
|
[12] {t("Automation - Demonstration of developed features")}
|
2025-04-26 17:12:38 -04:00
|
|
|
|
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
── {t("Deployment, network & security")} ──
|
[ADD] transform data : anonymiser un fichier externe ou une base Odoo
Partager un fichier ou une base demandait d'anonymiser à la main,
colonne par colonne, ou de ne pas partager. L'entrée ouvre un fichier
externe — Excel, Access, CSV, XML, JSON — ou une base Odoo, dit ce
qu'elle porte, puis en tire une copie. L'original n'est jamais touché.
Deux règles gouvernent ce qui sort. Une colonne n'est laissée intacte
que sur son CONTENU mesuré, hors onze étiquettes structurelles : décider
sur l'étiquette recopiait les colonnes les plus identifiantes en les
annonçant protégées. Et la copie est relue octet par octet, un classeur
portant de la donnée en une douzaine d'endroits hors des cellules.
Vérifié : 5 748 tests — 13 rouges, déjà rouges sur master.
--- EN ---
Sharing a file or a database meant anonymising by hand, column by
column, or not sharing. The entry opens an external file — Excel,
Access, CSV, XML, JSON — or an Odoo database, says what it holds, then
draws a copy of it. The original is never touched.
Two rules govern what comes out. A column is left alone on its measured
CONTENT, bar eleven structural labels: deciding on the label copied the
most identifying columns verbatim while announcing them as protected.
And the copy is reread byte by byte, a workbook holding data in a dozen
places that are not cells.
Checked: 5,748 tests — 13 red, already red on master.
Assisted-by: Claude Opus 5
2026-09-10 23:58:01 -04:00
|
|
|
|
[13] {t("Deploy - Deploy ERPLibre locally")}
|
|
|
|
|
|
[14] {t("Network - Network tools")}
|
|
|
|
|
|
[15] {t("Security - Dependency security audit")}
|
2026-03-12 05:31:45 -04:00
|
|
|
|
[0] {t("Back")}
|
2025-04-26 17:12:38 -04:00
|
|
|
|
"""
|
|
|
|
|
|
while True:
|
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
|
print()
|
|
|
|
|
|
if status == "0":
|
|
|
|
|
|
return
|
|
|
|
|
|
elif status == "1":
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
status = self.prompt_execute_code()
|
2025-04-26 17:12:38 -04:00
|
|
|
|
if status is not False:
|
|
|
|
|
|
return
|
|
|
|
|
|
elif status == "2":
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
status = self.prompt_execute_config()
|
2025-04-26 17:12:38 -04:00
|
|
|
|
if status is not False:
|
|
|
|
|
|
return
|
2025-05-04 01:02:12 -04:00
|
|
|
|
elif status == "3":
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
status = self.prompt_execute_instance()
|
2025-05-04 01:02:12 -04:00
|
|
|
|
if status is not False:
|
|
|
|
|
|
return
|
|
|
|
|
|
elif status == "4":
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
status = self.prompt_execute_test()
|
2025-05-04 01:02:12 -04:00
|
|
|
|
if status is not False:
|
|
|
|
|
|
return
|
2025-10-31 01:10:54 -04:00
|
|
|
|
elif status == "5":
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
status = self.prompt_execute_process()
|
2025-10-31 01:10:54 -04:00
|
|
|
|
if status is not False:
|
|
|
|
|
|
return
|
|
|
|
|
|
elif status == "6":
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
status = self.prompt_execute_database()
|
2025-10-31 01:10:54 -04:00
|
|
|
|
if status is not False:
|
|
|
|
|
|
return
|
2025-12-22 04:02:44 -05:00
|
|
|
|
elif status == "7":
|
[ADD] analyse: inspect an Odoo database without restoring it
A read-only toolkit answering « what is in this database, and what will
break on a version bump »: schema size and tables no model claims,
customised views compared with their module source, and the x_ fields
Studio left behind. It reads a backup zip directly, so a 40 GB dump needs
no restore, and it reaches every answer from the TODO menu.
--- FR ---
Une boîte à outils en lecture seule qui répond à « qu'y a-t-il dans cette
base, et qu'est-ce qui cassera à la montée de version » : taille du schéma
et tables qu'aucun modèle ne réclame, vues personnalisées comparées à leur
source, champs x_ laissés par Studio. Elle lit un zip de sauvegarde tel
quel, donc sans restaurer, et tout s'atteint depuis le menu TODO.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
status = self.prompt_execute_analyse()
|
2025-12-22 04:02:44 -05:00
|
|
|
|
if status is not False:
|
|
|
|
|
|
return
|
2026-02-13 03:46:27 -05:00
|
|
|
|
elif status == "8":
|
[ADD] transform data : anonymiser un fichier externe ou une base Odoo
Partager un fichier ou une base demandait d'anonymiser à la main,
colonne par colonne, ou de ne pas partager. L'entrée ouvre un fichier
externe — Excel, Access, CSV, XML, JSON — ou une base Odoo, dit ce
qu'elle porte, puis en tire une copie. L'original n'est jamais touché.
Deux règles gouvernent ce qui sort. Une colonne n'est laissée intacte
que sur son CONTENU mesuré, hors onze étiquettes structurelles : décider
sur l'étiquette recopiait les colonnes les plus identifiantes en les
annonçant protégées. Et la copie est relue octet par octet, un classeur
portant de la donnée en une douzaine d'endroits hors des cellules.
Vérifié : 5 748 tests — 13 rouges, déjà rouges sur master.
--- EN ---
Sharing a file or a database meant anonymising by hand, column by
column, or not sharing. The entry opens an external file — Excel,
Access, CSV, XML, JSON — or an Odoo database, says what it holds, then
draws a copy of it. The original is never touched.
Two rules govern what comes out. A column is left alone on its measured
CONTENT, bar eleven structural labels: deciding on the label copied the
most identifying columns verbatim while announcing them as protected.
And the copy is reread byte by byte, a workbook holding data in a dozen
places that are not cells.
Checked: 5,748 tests — 13 red, already red on master.
Assisted-by: Claude Opus 5
2026-09-10 23:58:01 -04:00
|
|
|
|
status = self.prompt_execute_transform()
|
2026-02-13 03:46:27 -05:00
|
|
|
|
if status is not False:
|
|
|
|
|
|
return
|
2026-02-28 03:24:22 -05:00
|
|
|
|
elif status == "9":
|
[ADD] transform data : anonymiser un fichier externe ou une base Odoo
Partager un fichier ou une base demandait d'anonymiser à la main,
colonne par colonne, ou de ne pas partager. L'entrée ouvre un fichier
externe — Excel, Access, CSV, XML, JSON — ou une base Odoo, dit ce
qu'elle porte, puis en tire une copie. L'original n'est jamais touché.
Deux règles gouvernent ce qui sort. Une colonne n'est laissée intacte
que sur son CONTENU mesuré, hors onze étiquettes structurelles : décider
sur l'étiquette recopiait les colonnes les plus identifiantes en les
annonçant protégées. Et la copie est relue octet par octet, un classeur
portant de la donnée en une douzaine d'endroits hors des cellules.
Vérifié : 5 748 tests — 13 rouges, déjà rouges sur master.
--- EN ---
Sharing a file or a database meant anonymising by hand, column by
column, or not sharing. The entry opens an external file — Excel,
Access, CSV, XML, JSON — or an Odoo database, says what it holds, then
draws a copy of it. The original is never touched.
Two rules govern what comes out. A column is left alone on its measured
CONTENT, bar eleven structural labels: deciding on the label copied the
most identifying columns verbatim while announcing them as protected.
And the copy is reread byte by byte, a workbook holding data in a dozen
places that are not cells.
Checked: 5,748 tests — 13 red, already red on master.
Assisted-by: Claude Opus 5
2026-09-10 23:58:01 -04:00
|
|
|
|
status = self.prompt_execute_git()
|
2026-02-28 03:24:22 -05:00
|
|
|
|
if status is not False:
|
|
|
|
|
|
return
|
2026-03-07 01:38:55 -05:00
|
|
|
|
elif status == "10":
|
[ADD] transform data : anonymiser un fichier externe ou une base Odoo
Partager un fichier ou une base demandait d'anonymiser à la main,
colonne par colonne, ou de ne pas partager. L'entrée ouvre un fichier
externe — Excel, Access, CSV, XML, JSON — ou une base Odoo, dit ce
qu'elle porte, puis en tire une copie. L'original n'est jamais touché.
Deux règles gouvernent ce qui sort. Une colonne n'est laissée intacte
que sur son CONTENU mesuré, hors onze étiquettes structurelles : décider
sur l'étiquette recopiait les colonnes les plus identifiantes en les
annonçant protégées. Et la copie est relue octet par octet, un classeur
portant de la donnée en une douzaine d'endroits hors des cellules.
Vérifié : 5 748 tests — 13 rouges, déjà rouges sur master.
--- EN ---
Sharing a file or a database meant anonymising by hand, column by
column, or not sharing. The entry opens an external file — Excel,
Access, CSV, XML, JSON — or an Odoo database, says what it holds, then
draws a copy of it. The original is never touched.
Two rules govern what comes out. A column is left alone on its measured
CONTENT, bar eleven structural labels: deciding on the label copied the
most identifying columns verbatim while announcing them as protected.
And the copy is reread byte by byte, a workbook holding data in a dozen
places that are not cells.
Checked: 5,748 tests — 13 red, already red on master.
Assisted-by: Claude Opus 5
2026-09-10 23:58:01 -04:00
|
|
|
|
status = self.prompt_execute_doc()
|
2026-03-07 01:38:55 -05:00
|
|
|
|
if status is not False:
|
|
|
|
|
|
return
|
2026-03-07 02:59:07 -05:00
|
|
|
|
elif status == "11":
|
[ADD] transform data : anonymiser un fichier externe ou une base Odoo
Partager un fichier ou une base demandait d'anonymiser à la main,
colonne par colonne, ou de ne pas partager. L'entrée ouvre un fichier
externe — Excel, Access, CSV, XML, JSON — ou une base Odoo, dit ce
qu'elle porte, puis en tire une copie. L'original n'est jamais touché.
Deux règles gouvernent ce qui sort. Une colonne n'est laissée intacte
que sur son CONTENU mesuré, hors onze étiquettes structurelles : décider
sur l'étiquette recopiait les colonnes les plus identifiantes en les
annonçant protégées. Et la copie est relue octet par octet, un classeur
portant de la donnée en une douzaine d'endroits hors des cellules.
Vérifié : 5 748 tests — 13 rouges, déjà rouges sur master.
--- EN ---
Sharing a file or a database meant anonymising by hand, column by
column, or not sharing. The entry opens an external file — Excel,
Access, CSV, XML, JSON — or an Odoo database, says what it holds, then
draws a copy of it. The original is never touched.
Two rules govern what comes out. A column is left alone on its measured
CONTENT, bar eleven structural labels: deciding on the label copied the
most identifying columns verbatim while announcing them as protected.
And the copy is reread byte by byte, a workbook holding data in a dozen
places that are not cells.
Checked: 5,748 tests — 13 red, already red on master.
Assisted-by: Claude Opus 5
2026-09-10 23:58:01 -04:00
|
|
|
|
status = self.prompt_execute_gpt_code()
|
2026-03-08 15:53:51 -04:00
|
|
|
|
if status is not False:
|
|
|
|
|
|
return
|
|
|
|
|
|
elif status == "12":
|
[ADD] transform data : anonymiser un fichier externe ou une base Odoo
Partager un fichier ou une base demandait d'anonymiser à la main,
colonne par colonne, ou de ne pas partager. L'entrée ouvre un fichier
externe — Excel, Access, CSV, XML, JSON — ou une base Odoo, dit ce
qu'elle porte, puis en tire une copie. L'original n'est jamais touché.
Deux règles gouvernent ce qui sort. Une colonne n'est laissée intacte
que sur son CONTENU mesuré, hors onze étiquettes structurelles : décider
sur l'étiquette recopiait les colonnes les plus identifiantes en les
annonçant protégées. Et la copie est relue octet par octet, un classeur
portant de la donnée en une douzaine d'endroits hors des cellules.
Vérifié : 5 748 tests — 13 rouges, déjà rouges sur master.
--- EN ---
Sharing a file or a database meant anonymising by hand, column by
column, or not sharing. The entry opens an external file — Excel,
Access, CSV, XML, JSON — or an Odoo database, says what it holds, then
draws a copy of it. The original is never touched.
Two rules govern what comes out. A column is left alone on its measured
CONTENT, bar eleven structural labels: deciding on the label copied the
most identifying columns verbatim while announcing them as protected.
And the copy is reread byte by byte, a workbook holding data in a dozen
places that are not cells.
Checked: 5,748 tests — 13 red, already red on master.
Assisted-by: Claude Opus 5
2026-09-10 23:58:01 -04:00
|
|
|
|
status = self.prompt_execute_function()
|
2026-03-09 16:34:43 -04:00
|
|
|
|
if status is not False:
|
|
|
|
|
|
return
|
|
|
|
|
|
elif status == "13":
|
[ADD] transform data : anonymiser un fichier externe ou une base Odoo
Partager un fichier ou une base demandait d'anonymiser à la main,
colonne par colonne, ou de ne pas partager. L'entrée ouvre un fichier
externe — Excel, Access, CSV, XML, JSON — ou une base Odoo, dit ce
qu'elle porte, puis en tire une copie. L'original n'est jamais touché.
Deux règles gouvernent ce qui sort. Une colonne n'est laissée intacte
que sur son CONTENU mesuré, hors onze étiquettes structurelles : décider
sur l'étiquette recopiait les colonnes les plus identifiantes en les
annonçant protégées. Et la copie est relue octet par octet, un classeur
portant de la donnée en une douzaine d'endroits hors des cellules.
Vérifié : 5 748 tests — 13 rouges, déjà rouges sur master.
--- EN ---
Sharing a file or a database meant anonymising by hand, column by
column, or not sharing. The entry opens an external file — Excel,
Access, CSV, XML, JSON — or an Odoo database, says what it holds, then
draws a copy of it. The original is never touched.
Two rules govern what comes out. A column is left alone on its measured
CONTENT, bar eleven structural labels: deciding on the label copied the
most identifying columns verbatim while announcing them as protected.
And the copy is reread byte by byte, a workbook holding data in a dozen
places that are not cells.
Checked: 5,748 tests — 13 red, already red on master.
Assisted-by: Claude Opus 5
2026-09-10 23:58:01 -04:00
|
|
|
|
status = self.prompt_execute_deploy()
|
2026-03-07 02:59:07 -05:00
|
|
|
|
if status is not False:
|
|
|
|
|
|
return
|
2026-03-10 02:05:53 -04:00
|
|
|
|
elif status == "14":
|
[ADD] transform data : anonymiser un fichier externe ou une base Odoo
Partager un fichier ou une base demandait d'anonymiser à la main,
colonne par colonne, ou de ne pas partager. L'entrée ouvre un fichier
externe — Excel, Access, CSV, XML, JSON — ou une base Odoo, dit ce
qu'elle porte, puis en tire une copie. L'original n'est jamais touché.
Deux règles gouvernent ce qui sort. Une colonne n'est laissée intacte
que sur son CONTENU mesuré, hors onze étiquettes structurelles : décider
sur l'étiquette recopiait les colonnes les plus identifiantes en les
annonçant protégées. Et la copie est relue octet par octet, un classeur
portant de la donnée en une douzaine d'endroits hors des cellules.
Vérifié : 5 748 tests — 13 rouges, déjà rouges sur master.
--- EN ---
Sharing a file or a database meant anonymising by hand, column by
column, or not sharing. The entry opens an external file — Excel,
Access, CSV, XML, JSON — or an Odoo database, says what it holds, then
draws a copy of it. The original is never touched.
Two rules govern what comes out. A column is left alone on its measured
CONTENT, bar eleven structural labels: deciding on the label copied the
most identifying columns verbatim while announcing them as protected.
And the copy is reread byte by byte, a workbook holding data in a dozen
places that are not cells.
Checked: 5,748 tests — 13 red, already red on master.
Assisted-by: Claude Opus 5
2026-09-10 23:58:01 -04:00
|
|
|
|
status = self.prompt_execute_network()
|
[ADD] analyse: inspect an Odoo database without restoring it
A read-only toolkit answering « what is in this database, and what will
break on a version bump »: schema size and tables no model claims,
customised views compared with their module source, and the x_ fields
Studio left behind. It reads a backup zip directly, so a 40 GB dump needs
no restore, and it reaches every answer from the TODO menu.
--- FR ---
Une boîte à outils en lecture seule qui répond à « qu'y a-t-il dans cette
base, et qu'est-ce qui cassera à la montée de version » : taille du schéma
et tables qu'aucun modèle ne réclame, vues personnalisées comparées à leur
source, champs x_ laissés par Studio. Elle lit un zip de sauvegarde tel
quel, donc sans restaurer, et tout s'atteint depuis le menu TODO.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
if status is not False:
|
|
|
|
|
|
return
|
|
|
|
|
|
elif status == "15":
|
[ADD] transform data : anonymiser un fichier externe ou une base Odoo
Partager un fichier ou une base demandait d'anonymiser à la main,
colonne par colonne, ou de ne pas partager. L'entrée ouvre un fichier
externe — Excel, Access, CSV, XML, JSON — ou une base Odoo, dit ce
qu'elle porte, puis en tire une copie. L'original n'est jamais touché.
Deux règles gouvernent ce qui sort. Une colonne n'est laissée intacte
que sur son CONTENU mesuré, hors onze étiquettes structurelles : décider
sur l'étiquette recopiait les colonnes les plus identifiantes en les
annonçant protégées. Et la copie est relue octet par octet, un classeur
portant de la donnée en une douzaine d'endroits hors des cellules.
Vérifié : 5 748 tests — 13 rouges, déjà rouges sur master.
--- EN ---
Sharing a file or a database meant anonymising by hand, column by
column, or not sharing. The entry opens an external file — Excel,
Access, CSV, XML, JSON — or an Odoo database, says what it holds, then
draws a copy of it. The original is never touched.
Two rules govern what comes out. A column is left alone on its measured
CONTENT, bar eleven structural labels: deciding on the label copied the
most identifying columns verbatim while announcing them as protected.
And the copy is reread byte by byte, a workbook holding data in a dozen
places that are not cells.
Checked: 5,748 tests — 13 red, already red on master.
Assisted-by: Claude Opus 5
2026-09-10 23:58:01 -04:00
|
|
|
|
status = self.prompt_execute_security()
|
|
|
|
|
|
if status is not False:
|
|
|
|
|
|
return
|
2025-04-26 17:12:38 -04:00
|
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(t("Command not found !"))
|
2025-04-26 17:12:38 -04:00
|
|
|
|
|
2026-08-07 03:22:26 -04:00
|
|
|
|
def prompt_install(self):
|
|
|
|
|
|
print("Detect first installation from code source.")
|
|
|
|
|
|
|
|
|
|
|
|
first_installation_input = (
|
|
|
|
|
|
input(
|
|
|
|
|
|
"💬 First system installation? This will process system installation"
|
|
|
|
|
|
" before (Y/N): "
|
|
|
|
|
|
)
|
|
|
|
|
|
.strip()
|
|
|
|
|
|
.lower()
|
|
|
|
|
|
)
|
|
|
|
|
|
if self._is_yes(first_installation_input):
|
|
|
|
|
|
cmd = "./script/version/update_env_version.py --install"
|
|
|
|
|
|
self.execute.exec_command_live(cmd, source_erplibre=True)
|
|
|
|
|
|
print("Wait after OS installation before continue.")
|
|
|
|
|
|
|
|
|
|
|
|
# First detect pycharm, need to be open before installation and close to increase speed
|
|
|
|
|
|
has_pycharm = False
|
|
|
|
|
|
has_pycharm_community = False
|
|
|
|
|
|
result = subprocess.run(
|
|
|
|
|
|
["which", "pycharm"],
|
|
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
|
|
stderr=subprocess.PIPE,
|
|
|
|
|
|
text=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
if result.returncode == 0:
|
|
|
|
|
|
has_pycharm = True
|
|
|
|
|
|
else:
|
|
|
|
|
|
result = subprocess.run(
|
|
|
|
|
|
["which", "pycharm-community"],
|
|
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
|
|
stderr=subprocess.PIPE,
|
|
|
|
|
|
text=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
has_pycharm_community = result.returncode == 0
|
|
|
|
|
|
if (has_pycharm or has_pycharm_community) and not os.path.exists(
|
|
|
|
|
|
".idea"
|
|
|
|
|
|
):
|
|
|
|
|
|
pycharm_configuration_input = (
|
|
|
|
|
|
input("💬 Open Pycharm? (Y/N): ").strip().lower()
|
|
|
|
|
|
)
|
|
|
|
|
|
if self._is_yes(pycharm_configuration_input):
|
|
|
|
|
|
pycharm_bin = "pycharm" if has_pycharm else "pycharm-community"
|
|
|
|
|
|
|
|
|
|
|
|
cmd = f"cd {os.getcwd()} && {pycharm_bin} ./"
|
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
|
cmd,
|
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
single_source_erplibre=False,
|
|
|
|
|
|
new_window=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
print(
|
|
|
|
|
|
"👹 WAIT and Close Pycharm when processing is done before continue"
|
|
|
|
|
|
" this guide."
|
|
|
|
|
|
)
|
|
|
|
|
|
# TODO detect last version supported
|
|
|
|
|
|
# cmd_intern = "./script/install/install_erplibre.sh"
|
|
|
|
|
|
key_i = 0
|
|
|
|
|
|
commands_begin = {
|
|
|
|
|
|
"q": (
|
|
|
|
|
|
"q",
|
[ADD] déploiement : un proxy SOCKS par SSH, et son mode d'emploi
« -L » relaie UN service ; « -D » ouvre un relais SOCKS, par lequel le
navigateur atteint n'importe quelle destination depuis la machine distante —
une interface qui n'écoute que sur sa boucle locale, un hôte de son réseau sans
route depuis ici. Le port par défaut est 1080, modifiable, et contrôlé libre
avant d'ouvrir. La saisie de l'adresse est celle de sshfs, désormais partagée :
l'alias de ~/.ssh/config part tel quel à ssh, faute de quoi son ProxyJump se
perdrait et une VM imbriquée deviendrait injoignable. Le réglage de Firefox
s'affiche AVANT le lancement, la commande ne rendant la main qu'au Ctrl+C.
Vérifié : 9 tests neufs, et la numérotation du menu que deux autres gardent.
--- EN ---
"-L" relays ONE service; "-D" opens a SOCKS relay, through which the browser
reaches any destination from the remote machine — an interface listening only
on its loopback, a host of its network with no route from here. The default
port is 1080, changeable, and checked free before opening. The address prompt
is sshfs's, now shared: the ~/.ssh/config alias goes to ssh as is, failing
which its ProxyJump would be lost and a nested VM become unreachable. The
Firefox settings print BEFORE the launch, the command only returning on
Ctrl+C.
Checked: 9 new tests, and the menu numbering two others guard.
Assisted-by: Claude Opus 5
2026-09-24 13:32:28 -04:00
|
|
|
|
"q: ERPLibre only without Odoo, with the required Python",
|
2026-08-07 03:22:26 -04:00
|
|
|
|
"./script/install/install_erplibre.sh",
|
|
|
|
|
|
),
|
|
|
|
|
|
"w": (
|
|
|
|
|
|
"w",
|
|
|
|
|
|
"w: Install all Odoo version with ERPLibre",
|
|
|
|
|
|
"make install_odoo_all_version",
|
|
|
|
|
|
),
|
|
|
|
|
|
"m": (
|
|
|
|
|
|
"m",
|
|
|
|
|
|
"m: ERPLibre with mobile home",
|
|
|
|
|
|
"./mobile/install_and_run.sh",
|
|
|
|
|
|
),
|
|
|
|
|
|
"0": (
|
|
|
|
|
|
"0",
|
|
|
|
|
|
f"0: {t('Quit')}",
|
|
|
|
|
|
),
|
|
|
|
|
|
}
|
|
|
|
|
|
commands_end = {}
|
|
|
|
|
|
versions, installed_versions, odoo_installed_version = (
|
|
|
|
|
|
get_odoo_version()
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
for version_info in versions[::-1]:
|
|
|
|
|
|
key_i += 1
|
|
|
|
|
|
key_s = str(key_i)
|
|
|
|
|
|
label = f"{key_s}: Odoo {version_info.get('odoo_version')}"
|
|
|
|
|
|
|
|
|
|
|
|
odoo_version = f"odoo{version_info.get('odoo_version')}"
|
|
|
|
|
|
if odoo_version in installed_versions:
|
|
|
|
|
|
label += " - Installed"
|
|
|
|
|
|
if odoo_version == odoo_installed_version:
|
|
|
|
|
|
label += " - Actual"
|
[FIX] script todo : lire « default » là où le fichier l'écrit ainsi
L'étiquette « - Default » ne paraissait jamais, ni au choix de version ni au
choix d'environnement : les deux lectures interrogeaient « Default » avec une
majuscule, quand conf/supported_version_erplibre.json écrit « default ».
dict.get rend None sur une clé absente, sans rien dire — la version par
défaut passait donc pour une version ordinaire, et rien ne le signalait.
Le garde-fou ne vérifie pas ce seul cas : il apparie les deux côtés, et toute
clé que le menu interroge doit exister dans le fichier. Vérifié : 4 tests,
dont deux tombent si la majuscule revient.
--- EN ---
The « - Default » label never appeared, neither on the version choice nor on
the environment one: both reads asked for « Default » with a capital, while
conf/supported_version_erplibre.json writes « default ». dict.get returns None
on a missing key without a word — the default version therefore looked like an
ordinary one, and nothing said otherwise.
The guard does not check that single case: it pairs both sides, and every key
the menu asks for must exist in the file. Checked: 4 tests, two of which fail
if the capital comes back.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
|
if version_info.get("default"):
|
2026-08-07 03:22:26 -04:00
|
|
|
|
label += " - Default"
|
|
|
|
|
|
if version_info.get("is_deprecated"):
|
|
|
|
|
|
label += " - Deprecated"
|
|
|
|
|
|
erplibre_version = version_info.get("erplibre_version")
|
|
|
|
|
|
commands_begin[key_s] = (
|
|
|
|
|
|
key_s,
|
|
|
|
|
|
label,
|
|
|
|
|
|
f"./script/version/update_env_version.py --erplibre_version {erplibre_version} --install_dev",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Add final command
|
|
|
|
|
|
install_commands = {**commands_begin, **commands_end}
|
|
|
|
|
|
|
|
|
|
|
|
# Show command
|
|
|
|
|
|
odoo_version_input = ""
|
|
|
|
|
|
while odoo_version_input not in install_commands:
|
|
|
|
|
|
if odoo_version_input:
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"{t('Error, cannot understand value')} '{odoo_version_input}'"
|
|
|
|
|
|
)
|
|
|
|
|
|
str_input_dyn_odoo_version = (
|
|
|
|
|
|
f"💬 {t('Choose a version:')}\n\t"
|
|
|
|
|
|
+ "\n\t".join([a[1] for a in install_commands.values()])
|
|
|
|
|
|
+ f"\n{t('Select: ')}"
|
|
|
|
|
|
)
|
|
|
|
|
|
odoo_version_input = (
|
|
|
|
|
|
input(str_input_dyn_odoo_version).strip().lower()
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if odoo_version_input == "0":
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
cmd_intern = install_commands.get(odoo_version_input)[2]
|
|
|
|
|
|
|
|
|
|
|
|
# For numbered version selections, offer extra modules sub-menu
|
|
|
|
|
|
if odoo_version_input.isdigit():
|
|
|
|
|
|
extra_choices = {
|
|
|
|
|
|
"1": (
|
|
|
|
|
|
"1",
|
|
|
|
|
|
f"1: {t('Standard install (without extra modules)')}",
|
|
|
|
|
|
),
|
|
|
|
|
|
"2": (
|
|
|
|
|
|
"2",
|
|
|
|
|
|
f"2: {t('Install with extra modules (CybroOdoo - large, slow)')}",
|
|
|
|
|
|
),
|
|
|
|
|
|
"0": ("0", f"0: {t('Back')}"),
|
|
|
|
|
|
}
|
|
|
|
|
|
extra_input = ""
|
|
|
|
|
|
while extra_input not in extra_choices:
|
|
|
|
|
|
if extra_input:
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"{t('Error, cannot understand value')} '{extra_input}'"
|
|
|
|
|
|
)
|
|
|
|
|
|
str_extra = (
|
|
|
|
|
|
f"💬 {t('Install type:')}\n\t"
|
|
|
|
|
|
+ "\n\t".join([a[1] for a in extra_choices.values()])
|
|
|
|
|
|
+ f"\n{t('Select: ')}"
|
|
|
|
|
|
)
|
|
|
|
|
|
extra_input = input(str_extra).strip()
|
|
|
|
|
|
if extra_input == "0":
|
|
|
|
|
|
return
|
|
|
|
|
|
if extra_input == "2":
|
|
|
|
|
|
cmd_intern = cmd_intern + " --with_extra"
|
|
|
|
|
|
|
|
|
|
|
|
print(f"{t('Will execute:')}\n{cmd_intern}")
|
|
|
|
|
|
|
|
|
|
|
|
# TODO use external script to detect terminal to use on system
|
|
|
|
|
|
# TODO check script open_terminal_code_generator.sh
|
|
|
|
|
|
# cmd_extern = f"gnome-terminal -- bash -c '{cmd_intern};bash'"
|
|
|
|
|
|
try:
|
|
|
|
|
|
subprocess.run(
|
|
|
|
|
|
cmd_intern, shell=True, executable="/bin/bash", check=True
|
|
|
|
|
|
)
|
|
|
|
|
|
except subprocess.CalledProcessError as e:
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"{t('The Bash script failed with return code')} {e.returncode}."
|
|
|
|
|
|
)
|
|
|
|
|
|
print("Wait after installation and open projects by terminal.")
|
|
|
|
|
|
print("make open_terminal")
|
|
|
|
|
|
self.restart_script(str(e))
|
|
|
|
|
|
|
2026-08-10 03:10:50 -04:00
|
|
|
|
def execute_from_configuration(
|
|
|
|
|
|
self, instance, exec_run_db=False, ignore_makefile=False
|
|
|
|
|
|
):
|
|
|
|
|
|
# exec_run_db need argument database
|
|
|
|
|
|
kdbx_key = instance.get("kdbx_key")
|
|
|
|
|
|
odoo_user = instance.get("user")
|
|
|
|
|
|
odoo_password = instance.get("password")
|
|
|
|
|
|
|
[FIX] security: the KeePass password leaves the command line too
Same exposure as the master password, same fix. kdbx_manager put the Odoo
password straight into the web_login command; /proc/<pid>/cmdline is
readable by every user on the machine, and no downstream filter reaches
that.
The command now carries the NAME of an environment variable, never the
value. One name per entry, because several credentials go out in a single
"parallel" call and a single variable could not tell them apart.
get_extra_command_user therefore returns (fragments, variables), and the
two call sites hand the variables to exec_command_live, which already
merged an environment.
Two things found on the way. web_login re-sent config.default_password_auth
when it retried after dismissing a modal, ignoring whatever the caller had
passed -- the retry silently fell back to "admin". And install_forgejo
printed the admin password back to the terminal, hence into the install log
and any CI capture; its own header already documents the default.
A test pins the guarantee: the fragment must not contain the password.
--- FR ---
Même exposition que pour le mot de passe maître, même correctif.
kdbx_manager mettait le mot de passe Odoo directement dans la commande
web_login ; /proc/<pid>/cmdline est lisible par tout utilisateur de la
machine, et aucun filtre en aval ne l'atteint.
La commande porte désormais le NOM d'une variable d'environnement, jamais
la valeur. Un nom par entrée, car plusieurs identifiants partent dans un
seul appel « parallel » et une variable unique ne saurait les distinguer.
get_extra_command_user rend donc (fragments, variables), et les deux
appelants confient les variables à exec_command_live, qui fusionnait déjà
un environnement.
Deux trouvailles en chemin. web_login renvoyait config.default_password_auth
à la reprise après une modale, ignorant ce que l'appelant avait fourni — la
reprise retombait en silence sur « admin ». Et install_forgejo réaffichait
le mot de passe administrateur, donc dans le journal d'installation et
toute capture de CI ; son propre en-tête documente déjà le défaut.
Un test verrouille la garantie : le fragment ne doit pas porter le secret.
Assisted-by: Claude Opus 5
2026-08-23 00:25:03 -04:00
|
|
|
|
# Le mot de passe voyage par l'environnement, jamais par argv : la
|
|
|
|
|
|
# ligne de commande est lisible par tout utilisateur de la machine.
|
|
|
|
|
|
web_login_env = {}
|
2026-08-10 03:10:50 -04:00
|
|
|
|
if kdbx_key:
|
[FIX] security: the KeePass password leaves the command line too
Same exposure as the master password, same fix. kdbx_manager put the Odoo
password straight into the web_login command; /proc/<pid>/cmdline is
readable by every user on the machine, and no downstream filter reaches
that.
The command now carries the NAME of an environment variable, never the
value. One name per entry, because several credentials go out in a single
"parallel" call and a single variable could not tell them apart.
get_extra_command_user therefore returns (fragments, variables), and the
two call sites hand the variables to exec_command_live, which already
merged an environment.
Two things found on the way. web_login re-sent config.default_password_auth
when it retried after dismissing a modal, ignoring whatever the caller had
passed -- the retry silently fell back to "admin". And install_forgejo
printed the admin password back to the terminal, hence into the install log
and any CI capture; its own header already documents the default.
A test pins the guarantee: the fragment must not contain the password.
--- FR ---
Même exposition que pour le mot de passe maître, même correctif.
kdbx_manager mettait le mot de passe Odoo directement dans la commande
web_login ; /proc/<pid>/cmdline est lisible par tout utilisateur de la
machine, et aucun filtre en aval ne l'atteint.
La commande porte désormais le NOM d'une variable d'environnement, jamais
la valeur. Un nom par entrée, car plusieurs identifiants partent dans un
seul appel « parallel » et une variable unique ne saurait les distinguer.
get_extra_command_user rend donc (fragments, variables), et les deux
appelants confient les variables à exec_command_live, qui fusionnait déjà
un environnement.
Deux trouvailles en chemin. web_login renvoyait config.default_password_auth
à la reprise après une modale, ignorant ce que l'appelant avait fourni — la
reprise retombait en silence sur « admin ». Et install_forgejo réaffichait
le mot de passe administrateur, donc dans le journal d'installation et
toute capture de CI ; son propre en-tête documente déjà le défaut.
Un test verrouille la garantie : le fragment ne doit pas porter le secret.
Assisted-by: Claude Opus 5
2026-08-23 00:25:03 -04:00
|
|
|
|
(
|
|
|
|
|
|
extra_cmd_web_login,
|
|
|
|
|
|
web_login_env,
|
|
|
|
|
|
) = self.kdbx_manager.get_extra_command_user(kdbx_key)
|
2026-08-10 03:10:50 -04:00
|
|
|
|
elif odoo_user and odoo_password:
|
[FIX] security: the KeePass password leaves the command line too
Same exposure as the master password, same fix. kdbx_manager put the Odoo
password straight into the web_login command; /proc/<pid>/cmdline is
readable by every user on the machine, and no downstream filter reaches
that.
The command now carries the NAME of an environment variable, never the
value. One name per entry, because several credentials go out in a single
"parallel" call and a single variable could not tell them apart.
get_extra_command_user therefore returns (fragments, variables), and the
two call sites hand the variables to exec_command_live, which already
merged an environment.
Two things found on the way. web_login re-sent config.default_password_auth
when it retried after dismissing a modal, ignoring whatever the caller had
passed -- the retry silently fell back to "admin". And install_forgejo
printed the admin password back to the terminal, hence into the install log
and any CI capture; its own header already documents the default.
A test pins the guarantee: the fragment must not contain the password.
--- FR ---
Même exposition que pour le mot de passe maître, même correctif.
kdbx_manager mettait le mot de passe Odoo directement dans la commande
web_login ; /proc/<pid>/cmdline est lisible par tout utilisateur de la
machine, et aucun filtre en aval ne l'atteint.
La commande porte désormais le NOM d'une variable d'environnement, jamais
la valeur. Un nom par entrée, car plusieurs identifiants partent dans un
seul appel « parallel » et une variable unique ne saurait les distinguer.
get_extra_command_user rend donc (fragments, variables), et les deux
appelants confient les variables à exec_command_live, qui fusionnait déjà
un environnement.
Deux trouvailles en chemin. web_login renvoyait config.default_password_auth
à la reprise après une modale, ignorant ce que l'appelant avait fourni — la
reprise retombait en silence sur « admin ». Et install_forgejo réaffichait
le mot de passe administrateur, donc dans le journal d'installation et
toute capture de CI ; son propre en-tête documente déjà le défaut.
Un test verrouille la garantie : le fragment ne doit pas porter le secret.
Assisted-by: Claude Opus 5
2026-08-23 00:25:03 -04:00
|
|
|
|
web_login_env = {"EL_WEB_LOGIN_PWD_0": odoo_password}
|
2026-08-10 03:10:50 -04:00
|
|
|
|
extra_cmd_web_login = (
|
[FIX] security: the KeePass password leaves the command line too
Same exposure as the master password, same fix. kdbx_manager put the Odoo
password straight into the web_login command; /proc/<pid>/cmdline is
readable by every user on the machine, and no downstream filter reaches
that.
The command now carries the NAME of an environment variable, never the
value. One name per entry, because several credentials go out in a single
"parallel" call and a single variable could not tell them apart.
get_extra_command_user therefore returns (fragments, variables), and the
two call sites hand the variables to exec_command_live, which already
merged an environment.
Two things found on the way. web_login re-sent config.default_password_auth
when it retried after dismissing a modal, ignoring whatever the caller had
passed -- the retry silently fell back to "admin". And install_forgejo
printed the admin password back to the terminal, hence into the install log
and any CI capture; its own header already documents the default.
A test pins the guarantee: the fragment must not contain the password.
--- FR ---
Même exposition que pour le mot de passe maître, même correctif.
kdbx_manager mettait le mot de passe Odoo directement dans la commande
web_login ; /proc/<pid>/cmdline est lisible par tout utilisateur de la
machine, et aucun filtre en aval ne l'atteint.
La commande porte désormais le NOM d'une variable d'environnement, jamais
la valeur. Un nom par entrée, car plusieurs identifiants partent dans un
seul appel « parallel » et une variable unique ne saurait les distinguer.
get_extra_command_user rend donc (fragments, variables), et les deux
appelants confient les variables à exec_command_live, qui fusionnait déjà
un environnement.
Deux trouvailles en chemin. web_login renvoyait config.default_password_auth
à la reprise après une modale, ignorant ce que l'appelant avait fourni — la
reprise retombait en silence sur « admin ». Et install_forgejo réaffichait
le mot de passe administrateur, donc dans le journal d'installation et
toute capture de CI ; son propre en-tête documente déjà le défaut.
Un test verrouille la garantie : le fragment ne doit pas porter le secret.
Assisted-by: Claude Opus 5
2026-08-23 00:25:03 -04:00
|
|
|
|
f" --default_email_auth {odoo_user}"
|
|
|
|
|
|
" --default_password_auth_env EL_WEB_LOGIN_PWD_0"
|
2026-08-10 03:10:50 -04:00
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
extra_cmd_web_login = ""
|
|
|
|
|
|
|
|
|
|
|
|
makefile_cmd = instance.get("makefile_cmd")
|
|
|
|
|
|
if makefile_cmd and not ignore_makefile:
|
|
|
|
|
|
status = self.execute.exec_command_live(
|
|
|
|
|
|
f"make {makefile_cmd}",
|
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
single_source_erplibre=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
if status:
|
|
|
|
|
|
_logger.error(
|
|
|
|
|
|
f"Status {status} - exit execute_from_configuration"
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
if exec_run_db:
|
|
|
|
|
|
db_name = instance.get("database")
|
|
|
|
|
|
self.prompt_execute_selenium_and_run_db(
|
[FIX] security: the KeePass password leaves the command line too
Same exposure as the master password, same fix. kdbx_manager put the Odoo
password straight into the web_login command; /proc/<pid>/cmdline is
readable by every user on the machine, and no downstream filter reaches
that.
The command now carries the NAME of an environment variable, never the
value. One name per entry, because several credentials go out in a single
"parallel" call and a single variable could not tell them apart.
get_extra_command_user therefore returns (fragments, variables), and the
two call sites hand the variables to exec_command_live, which already
merged an environment.
Two things found on the way. web_login re-sent config.default_password_auth
when it retried after dismissing a modal, ignoring whatever the caller had
passed -- the retry silently fell back to "admin". And install_forgejo
printed the admin password back to the terminal, hence into the install log
and any CI capture; its own header already documents the default.
A test pins the guarantee: the fragment must not contain the password.
--- FR ---
Même exposition que pour le mot de passe maître, même correctif.
kdbx_manager mettait le mot de passe Odoo directement dans la commande
web_login ; /proc/<pid>/cmdline est lisible par tout utilisateur de la
machine, et aucun filtre en aval ne l'atteint.
La commande porte désormais le NOM d'une variable d'environnement, jamais
la valeur. Un nom par entrée, car plusieurs identifiants partent dans un
seul appel « parallel » et une variable unique ne saurait les distinguer.
get_extra_command_user rend donc (fragments, variables), et les deux
appelants confient les variables à exec_command_live, qui fusionnait déjà
un environnement.
Deux trouvailles en chemin. web_login renvoyait config.default_password_auth
à la reprise après une modale, ignorant ce que l'appelant avait fourni — la
reprise retombait en silence sur « admin ». Et install_forgejo réaffichait
le mot de passe administrateur, donc dans le journal d'installation et
toute capture de CI ; son propre en-tête documente déjà le défaut.
Un test verrouille la garantie : le fragment ne doit pas porter le secret.
Assisted-by: Claude Opus 5
2026-08-23 00:25:03 -04:00
|
|
|
|
db_name,
|
|
|
|
|
|
extra_cmd_web_login=extra_cmd_web_login,
|
|
|
|
|
|
web_login_env=web_login_env,
|
2026-08-10 03:10:50 -04:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
bash_command = instance.get("bash_command")
|
|
|
|
|
|
if bash_command:
|
|
|
|
|
|
print(f"{t('Will execute:')} {bash_command}")
|
|
|
|
|
|
self.execute.exec_command_live(bash_command, source_erplibre=False)
|
|
|
|
|
|
|
|
|
|
|
|
# Clé de CONFIGURATION, pas une chaîne d'interface : le passage aux
|
|
|
|
|
|
# clés i18n en texte anglais (4fc15c3) a renommé celle-ci en
|
|
|
|
|
|
# « Command: », le libellé affiché. Plus aucune entrée de todo.json
|
|
|
|
|
|
# ne correspondait, et « Open ERPLibre with TODO 🤖 » ne faisait
|
|
|
|
|
|
# plus rien — sans erreur, puisque le `if` était simplement faux.
|
|
|
|
|
|
command = instance.get("command")
|
|
|
|
|
|
if command:
|
|
|
|
|
|
self.prompt_execute_selenium(
|
[FIX] security: the KeePass password leaves the command line too
Same exposure as the master password, same fix. kdbx_manager put the Odoo
password straight into the web_login command; /proc/<pid>/cmdline is
readable by every user on the machine, and no downstream filter reaches
that.
The command now carries the NAME of an environment variable, never the
value. One name per entry, because several credentials go out in a single
"parallel" call and a single variable could not tell them apart.
get_extra_command_user therefore returns (fragments, variables), and the
two call sites hand the variables to exec_command_live, which already
merged an environment.
Two things found on the way. web_login re-sent config.default_password_auth
when it retried after dismissing a modal, ignoring whatever the caller had
passed -- the retry silently fell back to "admin". And install_forgejo
printed the admin password back to the terminal, hence into the install log
and any CI capture; its own header already documents the default.
A test pins the guarantee: the fragment must not contain the password.
--- FR ---
Même exposition que pour le mot de passe maître, même correctif.
kdbx_manager mettait le mot de passe Odoo directement dans la commande
web_login ; /proc/<pid>/cmdline est lisible par tout utilisateur de la
machine, et aucun filtre en aval ne l'atteint.
La commande porte désormais le NOM d'une variable d'environnement, jamais
la valeur. Un nom par entrée, car plusieurs identifiants partent dans un
seul appel « parallel » et une variable unique ne saurait les distinguer.
get_extra_command_user rend donc (fragments, variables), et les deux
appelants confient les variables à exec_command_live, qui fusionnait déjà
un environnement.
Deux trouvailles en chemin. web_login renvoyait config.default_password_auth
à la reprise après une modale, ignorant ce que l'appelant avait fourni — la
reprise retombait en silence sur « admin ». Et install_forgejo réaffichait
le mot de passe administrateur, donc dans le journal d'installation et
toute capture de CI ; son propre en-tête documente déjà le défaut.
Un test verrouille la garantie : le fragment ne doit pas porter le secret.
Assisted-by: Claude Opus 5
2026-08-23 00:25:03 -04:00
|
|
|
|
command=command,
|
|
|
|
|
|
extra_cmd_web_login=extra_cmd_web_login,
|
|
|
|
|
|
web_login_env=web_login_env,
|
2026-08-10 03:10:50 -04:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
callback = instance.get("callback")
|
|
|
|
|
|
if callback:
|
|
|
|
|
|
callback(instance)
|
|
|
|
|
|
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
# Étiquettes du fil d'Ariane par méthode de menu. Le fil est dérivé de la
|
|
|
|
|
|
# pile d'appels (aucune méthode de menu à modifier). Labels courts et
|
|
|
|
|
|
# stables, pensés pour être copiés afin de situer précisément un menu.
|
|
|
|
|
|
_MENU_LABELS = {
|
|
|
|
|
|
"run": "TODO",
|
|
|
|
|
|
"prompt_execute": "Execute",
|
[ADD] analyse: inspect an Odoo database without restoring it
A read-only toolkit answering « what is in this database, and what will
break on a version bump »: schema size and tables no model claims,
customised views compared with their module source, and the x_ fields
Studio left behind. It reads a backup zip directly, so a 40 GB dump needs
no restore, and it reaches every answer from the TODO menu.
--- FR ---
Une boîte à outils en lecture seule qui répond à « qu'y a-t-il dans cette
base, et qu'est-ce qui cassera à la montée de version » : taille du schéma
et tables qu'aucun modèle ne réclame, vues personnalisées comparées à leur
source, champs x_ laissés par Studio. Elle lit un zip de sauvegarde tel
quel, donc sans restaurer, et tout s'atteint depuis le menu TODO.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
"prompt_assistant": "Assistant",
|
[ADD] assistant : serveur LLM reconnu, catalogue gpt, sessions locales
L'entrée « Question IA » envoyait toute question à une seule API distante, sur
un modèle figé et sans historique : aucun serveur local n'était atteignable,
et rien ne disait où partait le texte.
Deux règles gouvernent ce qui la remplace. Un port ne dit jamais qui répond —
un seul en héberge jusqu'à trois, et une famille réémet l'API d'une autre en
entier — donc l'identité se lit dans le CORPS, par une échelle ordonnée. Et
une adresse ne devient jamais du texte d'invite : le détecteur du dépôt ne
voit pas les noms d'hôte, d'où une poignée opaque à sa place.
Vérifié : 284 tests, et contre un serveur du réseau — reconnaissance,
capacités lues, conversation multi-tours, triage juste.
--- EN ---
The "AI question" entry sent every question to a single remote API, on a
frozen model and with no history: no local server was reachable, and nothing
said where the text was going.
Two rules govern what replaces it. A port never says who answers — one hosts
up to three products, and one family re-serves another's native API in full —
so identity is read from the BODY, by an ordered ladder. And an address never
becomes prompt text: the repository's detector does not see host names, hence
an opaque handle in its place.
Checked: 284 tests, and against a server on the network — recognition,
capabilities read, a multi-turn conversation, a correct triage.
Assisted-by: Claude Opus 5
2026-09-09 06:41:56 -04:00
|
|
|
|
"prompt_assistant_llm": "LLM",
|
[FIX] todo : onze menus sans miette, et un contrôle qui les voit
Onze sous-menus n'avaient pas d'étiquette de fil d'Ariane. Leur segment
manquait à l'en-tête, et la télémétrie les traitait comme des COMMANDES : ils
apparaissaient en feuille, sous leur nom de méthode brut.
Le contrôle qui devait l'attraper cherchait dans la table de répartition du
menu Exécution, donc il ne voyait que les sous-menus atteints depuis là — un
menu ouvert depuis ailleurs, ou défini dans un mixin, n'était jamais examiné.
Il cherche maintenant par ce que le code APPELLE, dans tout le paquet, et
trois exemptions périmées ont disparu avec lui. Cinq écrans restent exemptés
et disent pourquoi : ce sont des actions qui posent une question.
--- EN ---
Eleven submenus had no breadcrumb label. Their segment was missing from the
header, and telemetry treated them as COMMANDS: they showed up as leaves,
under their raw method name.
The check meant to catch this looked in the Execute menu's dispatch table, so
it only saw submenus reached from there — a menu opened from elsewhere, or
defined in a mixin, was never examined. It now searches by what the code
CALLS, across the whole package, and three stale exemptions went with it. Five
screens stay exempt and say why: they are actions that ask a question.
Assisted-by: Claude Opus 5
2026-09-09 04:42:39 -04:00
|
|
|
|
"_llm_servers": "Servers",
|
|
|
|
|
|
"_llm_search": "Search",
|
|
|
|
|
|
"_llm_search_remote": "Over SSH",
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
"prompt_install": "Install",
|
|
|
|
|
|
"prompt_execute_function": "Automation",
|
|
|
|
|
|
"prompt_execute_code": "Code",
|
|
|
|
|
|
"prompt_execute_config": "Config",
|
|
|
|
|
|
"prompt_execute_database": "Database",
|
[ADD] analyse: inspect an Odoo database without restoring it
A read-only toolkit answering « what is in this database, and what will
break on a version bump »: schema size and tables no model claims,
customised views compared with their module source, and the x_ fields
Studio left behind. It reads a backup zip directly, so a 40 GB dump needs
no restore, and it reaches every answer from the TODO menu.
--- FR ---
Une boîte à outils en lecture seule qui répond à « qu'y a-t-il dans cette
base, et qu'est-ce qui cassera à la montée de version » : taille du schéma
et tables qu'aucun modèle ne réclame, vues personnalisées comparées à leur
source, champs x_ laissés par Studio. Elle lit un zip de sauvegarde tel
quel, donc sans restaurer, et tout s'atteint depuis le menu TODO.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
"prompt_execute_analyse": "Analyse",
|
[ADD] transform data : anonymiser un fichier externe ou une base Odoo
Partager un fichier ou une base demandait d'anonymiser à la main,
colonne par colonne, ou de ne pas partager. L'entrée ouvre un fichier
externe — Excel, Access, CSV, XML, JSON — ou une base Odoo, dit ce
qu'elle porte, puis en tire une copie. L'original n'est jamais touché.
Deux règles gouvernent ce qui sort. Une colonne n'est laissée intacte
que sur son CONTENU mesuré, hors onze étiquettes structurelles : décider
sur l'étiquette recopiait les colonnes les plus identifiantes en les
annonçant protégées. Et la copie est relue octet par octet, un classeur
portant de la donnée en une douzaine d'endroits hors des cellules.
Vérifié : 5 748 tests — 13 rouges, déjà rouges sur master.
--- EN ---
Sharing a file or a database meant anonymising by hand, column by
column, or not sharing. The entry opens an external file — Excel,
Access, CSV, XML, JSON — or an Odoo database, says what it holds, then
draws a copy of it. The original is never touched.
Two rules govern what comes out. A column is left alone on its measured
CONTENT, bar eleven structural labels: deciding on the label copied the
most identifying columns verbatim while announcing them as protected.
And the copy is reread byte by byte, a workbook holding data in a dozen
places that are not cells.
Checked: 5,748 tests — 13 red, already red on master.
Assisted-by: Claude Opus 5
2026-09-10 23:58:01 -04:00
|
|
|
|
"prompt_execute_transform": "Transform data",
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
"prompt_execute_doc": "Doc",
|
|
|
|
|
|
"prompt_execute_git": "Git",
|
|
|
|
|
|
"prompt_execute_git_local_server": "Git local server",
|
[FIX] todo : onze menus sans miette, et un contrôle qui les voit
Onze sous-menus n'avaient pas d'étiquette de fil d'Ariane. Leur segment
manquait à l'en-tête, et la télémétrie les traitait comme des COMMANDES : ils
apparaissaient en feuille, sous leur nom de méthode brut.
Le contrôle qui devait l'attraper cherchait dans la table de répartition du
menu Exécution, donc il ne voyait que les sous-menus atteints depuis là — un
menu ouvert depuis ailleurs, ou défini dans un mixin, n'était jamais examiné.
Il cherche maintenant par ce que le code APPELLE, dans tout le paquet, et
trois exemptions périmées ont disparu avec lui. Cinq écrans restent exemptés
et disent pourquoi : ce sont des actions qui posent une question.
--- EN ---
Eleven submenus had no breadcrumb label. Their segment was missing from the
header, and telemetry treated them as COMMANDS: they showed up as leaves,
under their raw method name.
The check meant to catch this looked in the Execute menu's dispatch table, so
it only saw submenus reached from there — a menu opened from elsewhere, or
defined in a mixin, was never examined. It now searches by what the code
CALLS, across the whole package, and three stale exemptions went with it. Five
screens stay exempt and say why: they are actions that ask a question.
Assisted-by: Claude Opus 5
2026-09-09 04:42:39 -04:00
|
|
|
|
"_prompt_git_server_actions": "Actions",
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
"prompt_execute_gpt_code": "GPT code",
|
[FIX] todo : onze menus sans miette, et un contrôle qui les voit
Onze sous-menus n'avaient pas d'étiquette de fil d'Ariane. Leur segment
manquait à l'en-tête, et la télémétrie les traitait comme des COMMANDES : ils
apparaissaient en feuille, sous leur nom de méthode brut.
Le contrôle qui devait l'attraper cherchait dans la table de répartition du
menu Exécution, donc il ne voyait que les sous-menus atteints depuis là — un
menu ouvert depuis ailleurs, ou défini dans un mixin, n'était jamais examiné.
Il cherche maintenant par ce que le code APPELLE, dans tout le paquet, et
trois exemptions périmées ont disparu avec lui. Cinq écrans restent exemptés
et disent pourquoi : ce sont des actions qui posent une question.
--- EN ---
Eleven submenus had no breadcrumb label. Their segment was missing from the
header, and telemetry treated them as COMMANDS: they showed up as leaves,
under their raw method name.
The check meant to catch this looked in the Execute menu's dispatch table, so
it only saw submenus reached from there — a menu opened from elsewhere, or
defined in a mixin, was never examined. It now searches by what the code
CALLS, across the whole package, and three stale exemptions went with it. Five
screens stay exempt and say why: they are actions that ask a question.
Assisted-by: Claude Opus 5
2026-09-09 04:42:39 -04:00
|
|
|
|
"_prompt_claude_configs": "Claude configs",
|
|
|
|
|
|
"prompt_execute_claude_plugins": "Plugins",
|
[ADD] assistant : serveur LLM reconnu, catalogue gpt, sessions locales
L'entrée « Question IA » envoyait toute question à une seule API distante, sur
un modèle figé et sans historique : aucun serveur local n'était atteignable,
et rien ne disait où partait le texte.
Deux règles gouvernent ce qui la remplace. Un port ne dit jamais qui répond —
un seul en héberge jusqu'à trois, et une famille réémet l'API d'une autre en
entier — donc l'identité se lit dans le CORPS, par une échelle ordonnée. Et
une adresse ne devient jamais du texte d'invite : le détecteur du dépôt ne
voit pas les noms d'hôte, d'où une poignée opaque à sa place.
Vérifié : 284 tests, et contre un serveur du réseau — reconnaissance,
capacités lues, conversation multi-tours, triage juste.
--- EN ---
The "AI question" entry sent every question to a single remote API, on a
frozen model and with no history: no local server was reachable, and nothing
said where the text was going.
Two rules govern what replaces it. A port never says who answers — one hosts
up to three products, and one family re-serves another's native API in full —
so identity is read from the BODY, by an ordered ladder. And an address never
becomes prompt text: the repository's detector does not see host names, hence
an opaque handle in its place.
Checked: 284 tests, and against a server on the network — recognition,
capabilities read, a multi-turn conversation, a correct triage.
Assisted-by: Claude Opus 5
2026-09-09 06:41:56 -04:00
|
|
|
|
"prompt_claude_sessions": "Claude Code",
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
"prompt_execute_process": "Process",
|
|
|
|
|
|
"prompt_execute_instance": "Run",
|
|
|
|
|
|
"prompt_execute_rtk": "RTK",
|
|
|
|
|
|
"prompt_execute_update": "Update",
|
|
|
|
|
|
"prompt_execute_deploy": "Deploy",
|
|
|
|
|
|
"prompt_execute_deploy_ssh": "SSH",
|
[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
|
|
|
|
"prompt_execute_qemu_cache": "QEMU cache",
|
|
|
|
|
|
"_cache_service": "Service",
|
|
|
|
|
|
"_cache_tests": "Tests",
|
|
|
|
|
|
"_cache_age": "Age and cleanup",
|
|
|
|
|
|
"_cache_exceptions": "Exceptions",
|
|
|
|
|
|
"_cache_journaux": "Logs",
|
|
|
|
|
|
"_cache_miroir_git": "Git mirrors",
|
2026-09-16 03:00:17 -04:00
|
|
|
|
"_cache_nettoyage_auto": "Automatic cleanup",
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
"prompt_execute_qemu": "QEMU/KVM",
|
[FIX] todo : rendre au découpage ses colonnes de télémétrie
« Il manque plein d'informations qu'il y avait avant » : l'écran de
télémétrie construit son arbre en LISANT le code — un fichier, sa première
classe. Depuis que les menus QEMU/KVM et Proxmox vivent dans des mixins,
leurs colonnes avaient disparu de cet écran ; les commandes s'exécutaient
toujours, mais on ne pouvait plus les lancer de là. L'arbre lit maintenant
aussi les mixins, trouvés dans les imports de todo.py — un mixin ajouté
demain apparaîtra sans qu'on y pense.
Le menu Proxmox n'avait pas d'étiquette : son fil d'Ariane s'arrêtait deux
niveaux plus haut, sur « Deploy ». Vérifié : 16 commandes QEMU/KVM et 18
Proxmox dans l'arbre, contre zéro et zéro.
--- EN ---
"A lot of information that used to be there is missing": the telemetry screen
builds its tree by READING the code — one file, its first class. Since the
QEMU/KVM and Proxmox menus moved into mixins, their columns had vanished from
that screen; the commands still ran, but could no longer be launched from
there. The tree now reads the mixins too, found in todo.py's own imports — a
mixin added tomorrow shows up without anyone thinking about it.
The Proxmox menu had no label: its breadcrumb stopped two levels up, at
"Deploy". Verified: 16 QEMU/KVM commands and 18 Proxmox ones in the tree,
against zero and zero.
Assisted-by: Claude Opus 5
2026-08-24 04:17:56 -04:00
|
|
|
|
"prompt_execute_proxmox": "Proxmox VE",
|
[FIX] todo : onze menus sans miette, et un contrôle qui les voit
Onze sous-menus n'avaient pas d'étiquette de fil d'Ariane. Leur segment
manquait à l'en-tête, et la télémétrie les traitait comme des COMMANDES : ils
apparaissaient en feuille, sous leur nom de méthode brut.
Le contrôle qui devait l'attraper cherchait dans la table de répartition du
menu Exécution, donc il ne voyait que les sous-menus atteints depuis là — un
menu ouvert depuis ailleurs, ou défini dans un mixin, n'était jamais examiné.
Il cherche maintenant par ce que le code APPELLE, dans tout le paquet, et
trois exemptions périmées ont disparu avec lui. Cinq écrans restent exemptés
et disent pourquoi : ce sont des actions qui posent une question.
--- EN ---
Eleven submenus had no breadcrumb label. Their segment was missing from the
header, and telemetry treated them as COMMANDS: they showed up as leaves,
under their raw method name.
The check meant to catch this looked in the Execute menu's dispatch table, so
it only saw submenus reached from there — a menu opened from elsewhere, or
defined in a mixin, was never examined. It now searches by what the code
CALLS, across the whole package, and three stale exemptions went with it. Five
screens stay exempt and say why: they are actions that ask a question.
Assisted-by: Claude Opus 5
2026-09-09 04:42:39 -04:00
|
|
|
|
"prompt_execute_vpn": "VPN",
|
|
|
|
|
|
"prompt_execute_network": "Network",
|
|
|
|
|
|
"prompt_execute_security": "Security",
|
|
|
|
|
|
"prompt_execute_test": "Test",
|
|
|
|
|
|
"prompt_execute_longtest": "Long test",
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
"prompt_configuration": "Configuration",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-07 03:22:26 -04:00
|
|
|
|
def _menu_header(self):
|
|
|
|
|
|
"""En-tête de menu : fil d'Ariane (dérivé de la pile d'appels) suivi de
|
|
|
|
|
|
la ligne « Commande : ». Le fil situe le menu courant et se copie pour
|
|
|
|
|
|
décrire sans ambiguïté où l'on se trouve."""
|
|
|
|
|
|
crumbs = []
|
|
|
|
|
|
for frame_info in reversed(inspect.stack()):
|
|
|
|
|
|
if frame_info.frame.f_locals.get("self") is not self:
|
|
|
|
|
|
continue
|
|
|
|
|
|
label = self._MENU_LABELS.get(frame_info.function)
|
|
|
|
|
|
if label and (not crumbs or crumbs[-1] != label):
|
|
|
|
|
|
crumbs.append(label)
|
|
|
|
|
|
header = ""
|
|
|
|
|
|
if crumbs:
|
|
|
|
|
|
header = "📍 " + " › ".join(crumbs) + "\n"
|
|
|
|
|
|
# Télémétrie de navigation (best-effort, ne casse jamais le menu).
|
|
|
|
|
|
try:
|
|
|
|
|
|
from script.todo import todo_telemetry
|
|
|
|
|
|
|
|
|
|
|
|
todo_telemetry.record(" › ".join(crumbs))
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
return header + t("Command:")
|
|
|
|
|
|
|
2026-08-10 03:10:50 -04:00
|
|
|
|
def _todo_telemetry_tui(self):
|
|
|
|
|
|
"""Ouvre le TUI de télémétrie (arbre/Kanban). Une commande choisie est
|
|
|
|
|
|
exécutée au retour (hors du TUI) ; on propose ensuite de REVENIR (l'état
|
|
|
|
|
|
et la position du curseur sont restaurés) ou de quitter."""
|
|
|
|
|
|
from script.todo import textual_setup
|
|
|
|
|
|
from script.todo.todo_telemetry import run_tui
|
|
|
|
|
|
|
|
|
|
|
|
if not textual_setup.ensure():
|
|
|
|
|
|
return
|
|
|
|
|
|
state = None
|
|
|
|
|
|
while True:
|
|
|
|
|
|
try:
|
|
|
|
|
|
result = run_tui(state=state)
|
|
|
|
|
|
except ImportError:
|
|
|
|
|
|
return
|
|
|
|
|
|
if not result:
|
|
|
|
|
|
return
|
|
|
|
|
|
action, state = result
|
|
|
|
|
|
if not action:
|
|
|
|
|
|
return # quitté sans choisir de commande
|
|
|
|
|
|
method, kwargs = action
|
|
|
|
|
|
# Fil d'Ariane : la commande étant lancée DEPUIS la télémétrie (et
|
|
|
|
|
|
# non via la navigation), aucun menu n'a affiché le chemin. On le
|
|
|
|
|
|
# montre ici (dernier segment traduit + icône) et on l'enregistre.
|
|
|
|
|
|
path = state.get("path") if isinstance(state, dict) else None
|
|
|
|
|
|
if path:
|
|
|
|
|
|
segs = path.split(" › ")
|
|
|
|
|
|
segs[-1] = t(segs[-1])
|
|
|
|
|
|
print(f"\n📍 {' › '.join(segs)}")
|
|
|
|
|
|
try:
|
|
|
|
|
|
from script.todo import todo_telemetry
|
|
|
|
|
|
|
|
|
|
|
|
todo_telemetry.record(path)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
fn = getattr(self, method, None)
|
|
|
|
|
|
if not callable(fn):
|
|
|
|
|
|
print(f"{t('Command not found !')} ({method})")
|
|
|
|
|
|
else:
|
|
|
|
|
|
try:
|
|
|
|
|
|
fn(**(kwargs or {}))
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
print(f"{t('Command failed: ')}{exc}")
|
|
|
|
|
|
# Revenir (curseur restauré) ou quitter ?
|
|
|
|
|
|
ans = input(f"\n{t('Back to telemetry (r) or quit (Enter)? ')}")
|
|
|
|
|
|
if ans.strip().lower() not in ("r", "revenir", "o", "oui", "y"):
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2026-08-07 03:22:26 -04:00
|
|
|
|
# Préférences éditables depuis le menu Configuration : clé, libellé, et
|
|
|
|
|
|
# valeurs proposées (valeur stockée -> libellé affiché). Une seule table :
|
|
|
|
|
|
# l'écran, la lecture et l'écriture en découlent.
|
|
|
|
|
|
_PREF_CHOICES = {
|
|
|
|
|
|
"qemu_deploy_ui": (
|
|
|
|
|
|
"QEMU deployment interface",
|
|
|
|
|
|
(
|
|
|
|
|
|
("ask", "Ask every time"),
|
|
|
|
|
|
("tui", "TUI form"),
|
|
|
|
|
|
("cli", "Classic questions (line by line)"),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
"qemu_deploy_progress": (
|
|
|
|
|
|
"Display while deploying",
|
|
|
|
|
|
(
|
|
|
|
|
|
("cli", "CLI output (easy to copy)"),
|
|
|
|
|
|
("tui", "TUI, collapsible blocks per VM"),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
"migration_ui": (
|
|
|
|
|
|
"Odoo migration interface",
|
|
|
|
|
|
(
|
|
|
|
|
|
("ask", "Ask every time"),
|
|
|
|
|
|
("tui", "TUI form"),
|
|
|
|
|
|
("cli", "Classic questions (line by line)"),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-07 03:22:26 -04:00
|
|
|
|
def _pref_label(self, key):
|
|
|
|
|
|
"""Libellé traduit de la valeur courante d'une préférence."""
|
|
|
|
|
|
value = todo_prefs.get(key)
|
|
|
|
|
|
for stored, label in self._PREF_CHOICES[key][1]:
|
|
|
|
|
|
if stored == value:
|
|
|
|
|
|
return t(label)
|
|
|
|
|
|
return str(value)
|
|
|
|
|
|
|
|
|
|
|
|
def _pref_edit(self, key):
|
|
|
|
|
|
"""Fait choisir une valeur parmi celles proposées pour `key`."""
|
|
|
|
|
|
title, options = self._PREF_CHOICES[key]
|
|
|
|
|
|
current = todo_prefs.get(key)
|
|
|
|
|
|
print(f"\n{t(title)} :")
|
|
|
|
|
|
for i, (stored, label) in enumerate(options, 1):
|
|
|
|
|
|
star = " *" if stored == current else ""
|
|
|
|
|
|
print(f" [{i}] {t(label)}{star}")
|
|
|
|
|
|
sel = input(f"{t('Choice (number, blank = keep):')} ").strip()
|
|
|
|
|
|
try:
|
|
|
|
|
|
idx = int(sel) - 1
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
return
|
|
|
|
|
|
if 0 <= idx < len(options):
|
|
|
|
|
|
todo_prefs.set(key, options[idx][0])
|
|
|
|
|
|
print(f" ✅ {t(title)} : {self._pref_label(key)}")
|
|
|
|
|
|
|
|
|
|
|
|
def prompt_configuration(self):
|
|
|
|
|
|
"""Réglages persistants de l'utilisateur (~/.erplibre/todo_prefs.json).
|
|
|
|
|
|
La langue vit à part, dans env_var.sh, et garde son propre mécanisme.
|
|
|
|
|
|
"""
|
|
|
|
|
|
while True:
|
|
|
|
|
|
lang = "français" if get_lang() == "fr" else "English"
|
|
|
|
|
|
choices = [
|
|
|
|
|
|
{"section": t("Interface")},
|
|
|
|
|
|
{"prompt_description": f"{t('Language / Langue')} ({lang})"},
|
|
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": (
|
|
|
|
|
|
f"{t('QEMU deployment interface')} "
|
|
|
|
|
|
f"({self._pref_label('qemu_deploy_ui')})"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": (
|
|
|
|
|
|
f"{t('Display while deploying')} "
|
|
|
|
|
|
f"({self._pref_label('qemu_deploy_progress')})"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": (
|
|
|
|
|
|
f"{t('Odoo migration interface')} "
|
|
|
|
|
|
f"({self._pref_label('migration_ui')})"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
[IMP] todo menus: language only in Configuration, Fork moved there
Execute carried a Preferences section whose one entry, the language, is
already Configuration's first item: two places for one setting. It is
removed, with its two unused translations. Fork, which opens a second TODO,
leaves the main menu for Configuration; the main menu now numbers Telemetry
4 and Configuration 5, and Reset all preferences becomes 6.
Checked: Configuration displayed with Fork at 5; 108 tests pass.
--- FR ---
[IMP] menus todo : langue dans Configuration seule, Fork y déménage
Execute portait une section Préférences dont la seule entrée, la langue, est
déjà la première de Configuration : deux endroits pour un réglage. Elle part,
avec ses deux traductions devenues inutiles. Fork, qui ouvre un second TODO,
quitte le menu principal pour Configuration ; le menu principal numérote
désormais Télémétrie 4 et Configuration 5, et la réinitialisation devient 6.
Vérifié : Configuration affichée avec Fork en 5 ; 108 tests passent.
Assisted-by: Claude Opus 5.5
2026-09-25 01:41:48 -04:00
|
|
|
|
{"prompt_description": t("Fork - Open TODO in a new tab")},
|
2026-08-07 03:22:26 -04:00
|
|
|
|
{"section": t("Maintenance")},
|
|
|
|
|
|
{"prompt_description": t("Reset all preferences")},
|
|
|
|
|
|
]
|
|
|
|
|
|
status = click.prompt(self.fill_help_info(choices))
|
|
|
|
|
|
print()
|
|
|
|
|
|
if status == "0":
|
|
|
|
|
|
return
|
|
|
|
|
|
elif status == "1":
|
|
|
|
|
|
self._change_language()
|
|
|
|
|
|
elif status == "2":
|
|
|
|
|
|
self._pref_edit("qemu_deploy_ui")
|
|
|
|
|
|
elif status == "3":
|
|
|
|
|
|
self._pref_edit("qemu_deploy_progress")
|
|
|
|
|
|
elif status == "4":
|
|
|
|
|
|
self._pref_edit("migration_ui")
|
|
|
|
|
|
elif status == "5":
|
[IMP] todo menus: language only in Configuration, Fork moved there
Execute carried a Preferences section whose one entry, the language, is
already Configuration's first item: two places for one setting. It is
removed, with its two unused translations. Fork, which opens a second TODO,
leaves the main menu for Configuration; the main menu now numbers Telemetry
4 and Configuration 5, and Reset all preferences becomes 6.
Checked: Configuration displayed with Fork at 5; 108 tests pass.
--- FR ---
[IMP] menus todo : langue dans Configuration seule, Fork y déménage
Execute portait une section Préférences dont la seule entrée, la langue, est
déjà la première de Configuration : deux endroits pour un réglage. Elle part,
avec ses deux traductions devenues inutiles. Fork, qui ouvre un second TODO,
quitte le menu principal pour Configuration ; le menu principal numérote
désormais Télémétrie 4 et Configuration 5, et la réinitialisation devient 6.
Vérifié : Configuration affichée avec Fork en 5 ; 108 tests passent.
Assisted-by: Claude Opus 5.5
2026-09-25 01:41:48 -04:00
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
|
"make todo", source_erplibre=True
|
|
|
|
|
|
)
|
|
|
|
|
|
elif status == "6":
|
2026-08-07 03:22:26 -04:00
|
|
|
|
n = todo_prefs.reset()
|
|
|
|
|
|
print(f"✅ {t('Preferences reset')} ({n})")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(t("Command not found !"))
|
|
|
|
|
|
|
2026-03-10 03:45:11 -04:00
|
|
|
|
def fill_help_info(self, choices):
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
# Une entrée {"section": "..."} affiche un titre de section SANS
|
|
|
|
|
|
# consommer de numéro : la numérotation reste continue sur les vraies
|
|
|
|
|
|
# commandes (compatible avec les elif codés en dur des menus).
|
|
|
|
|
|
help_info = self._menu_header() + "\n"
|
2026-03-12 05:31:45 -04:00
|
|
|
|
help_end = f"[0] {t('Back')}\n"
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
n = 0
|
|
|
|
|
|
for instance in choices:
|
|
|
|
|
|
section = instance.get("section")
|
|
|
|
|
|
if section:
|
|
|
|
|
|
help_info += f"\n── {section} ──\n"
|
|
|
|
|
|
continue
|
|
|
|
|
|
n += 1
|
2026-03-10 03:45:11 -04:00
|
|
|
|
desc_key = instance.get("prompt_description_key")
|
2026-03-07 02:59:07 -05:00
|
|
|
|
if desc_key:
|
|
|
|
|
|
desc = t(desc_key)
|
|
|
|
|
|
else:
|
2026-03-10 03:45:11 -04:00
|
|
|
|
desc = instance["prompt_description"]
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
help_info += f"[{n}] " + desc + "\n"
|
2025-04-26 17:12:38 -04:00
|
|
|
|
help_info += help_end
|
2025-04-28 00:32:51 -04:00
|
|
|
|
return help_info
|
|
|
|
|
|
|
|
|
|
|
|
def prompt_execute_instance(self):
|
2025-05-04 01:02:12 -04:00
|
|
|
|
# TODO proposer le déploiement à distance
|
|
|
|
|
|
# TODO proposer l'exécution de docker
|
|
|
|
|
|
# TODO proposer la création de docker
|
2026-03-10 03:45:11 -04:00
|
|
|
|
choices = self.config_file.get_config("instance")
|
|
|
|
|
|
init_len = len(choices)
|
2025-10-01 04:53:08 -04:00
|
|
|
|
|
2026-02-28 01:35:32 -05:00
|
|
|
|
# Support mobile ERPLibre
|
2025-12-27 05:23:48 -05:00
|
|
|
|
if os.path.exists(MOBILE_HOME_PATH):
|
2026-03-10 03:45:11 -04:00
|
|
|
|
menu_entry = {
|
2026-03-12 05:31:45 -04:00
|
|
|
|
"prompt_description": t("Mobile - Compile and run software"),
|
2025-10-01 04:53:08 -04:00
|
|
|
|
"callback": self.callback_make_mobile_home,
|
|
|
|
|
|
}
|
2026-03-10 03:45:11 -04:00
|
|
|
|
choices.append(menu_entry)
|
2026-02-28 01:35:32 -05:00
|
|
|
|
|
|
|
|
|
|
# Support custom database to execute
|
2026-03-10 03:45:11 -04:00
|
|
|
|
menu_entry = {
|
2026-03-12 05:31:45 -04:00
|
|
|
|
"prompt_description": t("Choose your database"),
|
2026-02-28 01:35:32 -05:00
|
|
|
|
"callback": self.callback_execute_custom_database,
|
|
|
|
|
|
}
|
2026-03-10 03:45:11 -04:00
|
|
|
|
choices.insert(0, menu_entry)
|
|
|
|
|
|
help_info = self.fill_help_info(choices)
|
2025-04-26 17:12:38 -04:00
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
|
print()
|
|
|
|
|
|
if status == "0":
|
|
|
|
|
|
return False
|
|
|
|
|
|
else:
|
|
|
|
|
|
cmd_no_found = True
|
|
|
|
|
|
try:
|
|
|
|
|
|
int_cmd = int(status)
|
2026-02-28 01:35:32 -05:00
|
|
|
|
if 1 < int_cmd <= init_len:
|
2025-04-26 17:12:38 -04:00
|
|
|
|
cmd_no_found = False
|
2026-03-13 15:04:14 -04:00
|
|
|
|
status = click.confirm(
|
|
|
|
|
|
t("Do you want a new instance?")
|
|
|
|
|
|
)
|
2026-03-10 03:45:11 -04:00
|
|
|
|
instance = choices[int_cmd - 1]
|
2025-04-28 00:32:51 -04:00
|
|
|
|
self.execute_from_configuration(
|
2026-03-10 03:45:11 -04:00
|
|
|
|
instance,
|
2025-04-28 00:32:51 -04:00
|
|
|
|
exec_run_db=True,
|
|
|
|
|
|
ignore_makefile=not bool(status),
|
|
|
|
|
|
)
|
2026-03-10 03:45:11 -04:00
|
|
|
|
elif int_cmd <= len(choices) or 1 == int_cmd:
|
2026-02-28 01:35:32 -05:00
|
|
|
|
cmd_no_found = False
|
2025-10-01 04:53:08 -04:00
|
|
|
|
# Execute dynamic instance
|
2026-03-10 03:45:11 -04:00
|
|
|
|
instance = choices[int_cmd - 1]
|
2025-10-01 04:53:08 -04:00
|
|
|
|
self.execute_from_configuration(
|
2026-03-10 03:45:11 -04:00
|
|
|
|
instance,
|
2025-10-01 04:53:08 -04:00
|
|
|
|
)
|
2025-04-26 17:12:38 -04:00
|
|
|
|
except ValueError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
if cmd_no_found:
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(t("Command not found !"))
|
2025-04-26 17:12:38 -04:00
|
|
|
|
|
2026-03-10 03:45:11 -04:00
|
|
|
|
def prompt_execute_function(self):
|
|
|
|
|
|
choices = self.config_file.get_config("function")
|
|
|
|
|
|
help_info = self.fill_help_info(choices)
|
2025-04-26 17:12:38 -04:00
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
|
print()
|
|
|
|
|
|
if status == "0":
|
|
|
|
|
|
return False
|
|
|
|
|
|
else:
|
|
|
|
|
|
cmd_no_found = True
|
|
|
|
|
|
try:
|
|
|
|
|
|
int_cmd = int(status)
|
2026-03-10 03:45:11 -04:00
|
|
|
|
if 0 < int_cmd <= len(choices):
|
2025-04-26 17:12:38 -04:00
|
|
|
|
cmd_no_found = False
|
2026-03-10 03:45:11 -04:00
|
|
|
|
instance = choices[int_cmd - 1]
|
|
|
|
|
|
self.execute_from_configuration(instance)
|
2025-04-26 17:12:38 -04:00
|
|
|
|
except ValueError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
if cmd_no_found:
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(t("Command not found !"))
|
2025-04-26 17:12:38 -04:00
|
|
|
|
|
2025-05-04 01:02:12 -04:00
|
|
|
|
def prompt_execute_update(self):
|
2026-02-13 01:27:38 -05:00
|
|
|
|
# self.execute.exec_command_live(f"make {makefile_cmd}")
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(f"🤖 {t('Development update')}")
|
2025-05-04 01:02:12 -04:00
|
|
|
|
# TODO détecter les modules en modification pour faire la mise à jour en cours
|
|
|
|
|
|
# TODO demander sur quel BD faire la mise à jour
|
|
|
|
|
|
# TODO proposer les modules manuelles selon la configuration à mettre à jour
|
|
|
|
|
|
# TODO proposer la mise à jour de l'IDE
|
|
|
|
|
|
# TODO proposer la mise à jour des git-repo
|
2025-10-31 01:10:54 -04:00
|
|
|
|
# TODO faire la mise à jour de ERPLibre
|
|
|
|
|
|
# TODO faire l'upgrade d'un odoo vers un autre
|
|
|
|
|
|
|
2026-03-10 03:45:11 -04:00
|
|
|
|
choices = self.config_file.get_config("update_from_makefile")
|
|
|
|
|
|
menu_entry = {
|
2026-03-12 05:31:45 -04:00
|
|
|
|
"prompt_description": t("Upgrade Odoo - Migration Database"),
|
2025-10-31 01:10:54 -04:00
|
|
|
|
}
|
2026-03-10 03:45:11 -04:00
|
|
|
|
choices.append(menu_entry)
|
|
|
|
|
|
poetry_entry = {
|
2026-03-12 05:31:45 -04:00
|
|
|
|
"prompt_description": t("Upgrade Poetry - Dependency of Odoo"),
|
2025-10-31 01:10:54 -04:00
|
|
|
|
}
|
2026-03-10 03:45:11 -04:00
|
|
|
|
choices.append(poetry_entry)
|
|
|
|
|
|
help_info = self.fill_help_info(choices)
|
2025-05-04 01:02:12 -04:00
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
|
print()
|
|
|
|
|
|
if status == "0":
|
|
|
|
|
|
return False
|
2026-03-10 03:45:11 -04:00
|
|
|
|
elif status == str(len(choices) - 1):
|
2025-10-31 01:10:54 -04:00
|
|
|
|
upgrade = todo_upgrade.TodoUpgrade(self)
|
[ADD] migration: go back a step from a prompt, and name the DB
Realising at a prompt that an earlier step deserved another answer had one
way out: Ctrl+C. That leaves the progression as it stands and makes you find
the resume screen again to rewind. « b » does it properly — it shows the
steps, rewinds the state, writes it, and says what to relaunch.
Cancelling that must not stop the migration, which is the trap: it returns to
the same prompt, exactly where you were. Removing that guard makes two tests
fail.
The COW warning printed « -d DB -t odooXX.0 » for commands meant to be pasted.
It knows both values, so it prints them, and offers --shape as well since that
is half the answer.
Checked on the four outcomes: a normal answer passes through, « b » then a
step rewinds and stops, cancelling and an unknown step both continue. 15 tests.
--- FR ---
S'apercevoir à une invite qu'une étape antérieure méritait un autre choix
n'avait qu'une issue : Ctrl+C. Cela laisse la progression telle quelle et
oblige à retrouver l'écran de reprise pour rembobiner. « b » le fait
proprement — il montre les étapes, rembobine l'état, l'écrit, et dit quoi
relancer.
Y renoncer ne doit pas arrêter la migration, et c'est le piège : on revient à
la même invite, exactement là où l'on était. Retirer ce garde-fou fait tomber
deux tests.
L'avertissement COW affichait « -d DB -t odooXX.0 » pour des commandes faites
pour être collées. Il connaît les deux valeurs, donc il les écrit, et propose
aussi --shape puisque c'est la moitié de la réponse.
Vérifié sur les quatre issues : une réponse normale passe, « b » puis une
étape rembobine et arrête, annuler et un choix inconnu continuent. 15 tests.
Assisted-by: Claude Opus 5
2026-08-12 01:47:44 -04:00
|
|
|
|
try:
|
|
|
|
|
|
upgrade.execute_odoo_upgrade()
|
|
|
|
|
|
except todo_upgrade.MigrationRewind:
|
|
|
|
|
|
# L'état est déjà rembobiné et écrit : il ne reste qu'à
|
|
|
|
|
|
# relancer, et l'écran de reprise repartira de l'étape
|
|
|
|
|
|
# choisie. Sortir d'ici plutôt que de rappeler la méthode
|
|
|
|
|
|
# évite de la reprendre au milieu de son état local.
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"\n⏪ {t('Rewound.')}"
|
|
|
|
|
|
f" {t('Relaunch the migration to resume from there.')}"
|
|
|
|
|
|
)
|
2026-03-10 03:45:11 -04:00
|
|
|
|
elif status == str(len(choices)):
|
2025-10-31 01:10:54 -04:00
|
|
|
|
self.upgrade_poetry()
|
2025-05-04 01:02:12 -04:00
|
|
|
|
else:
|
|
|
|
|
|
cmd_no_found = True
|
|
|
|
|
|
try:
|
2025-10-31 01:10:54 -04:00
|
|
|
|
int_cmd = int(status) - 1
|
2026-03-10 03:45:11 -04:00
|
|
|
|
if 0 < int_cmd <= len(choices):
|
2025-05-04 01:02:12 -04:00
|
|
|
|
cmd_no_found = False
|
2026-03-10 03:45:11 -04:00
|
|
|
|
instance = choices[int_cmd - 1]
|
|
|
|
|
|
self.execute_from_configuration(instance)
|
2025-05-04 01:02:12 -04:00
|
|
|
|
except ValueError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
if cmd_no_found:
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(t("Command not found !"))
|
2025-05-04 01:02:12 -04:00
|
|
|
|
|
2026-03-12 05:40:22 -04:00
|
|
|
|
def prompt_execute_deploy(self):
|
2026-03-13 15:04:14 -04:00
|
|
|
|
print(f"🤖 {t('Deploy ERPLibre to a local directory!')}")
|
2026-03-12 05:40:22 -04:00
|
|
|
|
choices = [
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
{"section": t("Local")},
|
2026-03-13 15:04:14 -04:00
|
|
|
|
{"prompt_description": t("Clone ERPLibre locally (git clone)")},
|
2026-03-22 22:28:24 -04:00
|
|
|
|
{"prompt_description": t("Configure sshfs")},
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
|
"SSH port forwarding (open Odoo in the browser)"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
[ADD] déploiement : un proxy SOCKS par SSH, et son mode d'emploi
« -L » relaie UN service ; « -D » ouvre un relais SOCKS, par lequel le
navigateur atteint n'importe quelle destination depuis la machine distante —
une interface qui n'écoute que sur sa boucle locale, un hôte de son réseau sans
route depuis ici. Le port par défaut est 1080, modifiable, et contrôlé libre
avant d'ouvrir. La saisie de l'adresse est celle de sshfs, désormais partagée :
l'alias de ~/.ssh/config part tel quel à ssh, faute de quoi son ProxyJump se
perdrait et une VM imbriquée deviendrait injoignable. Le réglage de Firefox
s'affiche AVANT le lancement, la commande ne rendant la main qu'au Ctrl+C.
Vérifié : 9 tests neufs, et la numérotation du menu que deux autres gardent.
--- EN ---
"-L" relays ONE service; "-D" opens a SOCKS relay, through which the browser
reaches any destination from the remote machine — an interface listening only
on its loopback, a host of its network with no route from here. The default
port is 1080, changeable, and checked free before opening. The address prompt
is sshfs's, now shared: the ~/.ssh/config alias goes to ssh as is, failing
which its ProxyJump would be lost and a nested VM become unreachable. The
Firefox settings print BEFORE the launch, the command only returning on
Ctrl+C.
Checked: 9 new tests, and the menu numbering two others guard.
Assisted-by: Claude Opus 5
2026-09-24 13:32:28 -04:00
|
|
|
|
{"prompt_description": t("Configure a SOCKS proxy over SSH")},
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
{"section": t("Remote & services")},
|
|
|
|
|
|
{"prompt_description": t("SSH (remote host)...")},
|
|
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
|
"QEMU/KVM - Deploy an Ubuntu VM (libvirt)"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -04:00
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
|
"Proxmox VE - Deploy a VM on a remote host"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
|
"Deploy - Install NTFY notification server"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
[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
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
|
"QEMU cache - Download mirror for local VMs"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
[ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.
Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.
--- EN ---
`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.
The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.
Assisted-by: Claude Opus 5
2026-09-03 23:43:02 -04:00
|
|
|
|
{"section": t("VPN & tunnels")},
|
|
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
|
"VPN - Tunnels (L2TP/IPsec, WireGuard, OpenVPN...)"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
]
|
|
|
|
|
|
help_info = self.fill_help_info(choices)
|
|
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
|
print()
|
|
|
|
|
|
if status == "0":
|
|
|
|
|
|
return False
|
|
|
|
|
|
elif status == "1":
|
|
|
|
|
|
self._deploy_clone_erplibre()
|
|
|
|
|
|
elif status == "2":
|
|
|
|
|
|
self._configure_sshfs()
|
|
|
|
|
|
elif status == "3":
|
|
|
|
|
|
self._deploy_port_forward()
|
|
|
|
|
|
elif status == "4":
|
[ADD] déploiement : un proxy SOCKS par SSH, et son mode d'emploi
« -L » relaie UN service ; « -D » ouvre un relais SOCKS, par lequel le
navigateur atteint n'importe quelle destination depuis la machine distante —
une interface qui n'écoute que sur sa boucle locale, un hôte de son réseau sans
route depuis ici. Le port par défaut est 1080, modifiable, et contrôlé libre
avant d'ouvrir. La saisie de l'adresse est celle de sshfs, désormais partagée :
l'alias de ~/.ssh/config part tel quel à ssh, faute de quoi son ProxyJump se
perdrait et une VM imbriquée deviendrait injoignable. Le réglage de Firefox
s'affiche AVANT le lancement, la commande ne rendant la main qu'au Ctrl+C.
Vérifié : 9 tests neufs, et la numérotation du menu que deux autres gardent.
--- EN ---
"-L" relays ONE service; "-D" opens a SOCKS relay, through which the browser
reaches any destination from the remote machine — an interface listening only
on its loopback, a host of its network with no route from here. The default
port is 1080, changeable, and checked free before opening. The address prompt
is sshfs's, now shared: the ~/.ssh/config alias goes to ssh as is, failing
which its ProxyJump would be lost and a nested VM become unreachable. The
Firefox settings print BEFORE the launch, the command only returning on
Ctrl+C.
Checked: 9 new tests, and the menu numbering two others guard.
Assisted-by: Claude Opus 5
2026-09-24 13:32:28 -04:00
|
|
|
|
self._deploy_socks_proxy()
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
elif status == "5":
|
[ADD] déploiement : un proxy SOCKS par SSH, et son mode d'emploi
« -L » relaie UN service ; « -D » ouvre un relais SOCKS, par lequel le
navigateur atteint n'importe quelle destination depuis la machine distante —
une interface qui n'écoute que sur sa boucle locale, un hôte de son réseau sans
route depuis ici. Le port par défaut est 1080, modifiable, et contrôlé libre
avant d'ouvrir. La saisie de l'adresse est celle de sshfs, désormais partagée :
l'alias de ~/.ssh/config part tel quel à ssh, faute de quoi son ProxyJump se
perdrait et une VM imbriquée deviendrait injoignable. Le réglage de Firefox
s'affiche AVANT le lancement, la commande ne rendant la main qu'au Ctrl+C.
Vérifié : 9 tests neufs, et la numérotation du menu que deux autres gardent.
--- EN ---
"-L" relays ONE service; "-D" opens a SOCKS relay, through which the browser
reaches any destination from the remote machine — an interface listening only
on its loopback, a host of its network with no route from here. The default
port is 1080, changeable, and checked free before opening. The address prompt
is sshfs's, now shared: the ~/.ssh/config alias goes to ssh as is, failing
which its ProxyJump would be lost and a nested VM become unreachable. The
Firefox settings print BEFORE the launch, the command only returning on
Ctrl+C.
Checked: 9 new tests, and the menu numbering two others guard.
Assisted-by: Claude Opus 5
2026-09-24 13:32:28 -04:00
|
|
|
|
self.prompt_execute_deploy_ssh()
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
elif status == "6":
|
[ADD] déploiement : un proxy SOCKS par SSH, et son mode d'emploi
« -L » relaie UN service ; « -D » ouvre un relais SOCKS, par lequel le
navigateur atteint n'importe quelle destination depuis la machine distante —
une interface qui n'écoute que sur sa boucle locale, un hôte de son réseau sans
route depuis ici. Le port par défaut est 1080, modifiable, et contrôlé libre
avant d'ouvrir. La saisie de l'adresse est celle de sshfs, désormais partagée :
l'alias de ~/.ssh/config part tel quel à ssh, faute de quoi son ProxyJump se
perdrait et une VM imbriquée deviendrait injoignable. Le réglage de Firefox
s'affiche AVANT le lancement, la commande ne rendant la main qu'au Ctrl+C.
Vérifié : 9 tests neufs, et la numérotation du menu que deux autres gardent.
--- EN ---
"-L" relays ONE service; "-D" opens a SOCKS relay, through which the browser
reaches any destination from the remote machine — an interface listening only
on its loopback, a host of its network with no route from here. The default
port is 1080, changeable, and checked free before opening. The address prompt
is sshfs's, now shared: the ~/.ssh/config alias goes to ssh as is, failing
which its ProxyJump would be lost and a nested VM become unreachable. The
Firefox settings print BEFORE the launch, the command only returning on
Ctrl+C.
Checked: 9 new tests, and the menu numbering two others guard.
Assisted-by: Claude Opus 5
2026-09-24 13:32:28 -04:00
|
|
|
|
self.prompt_execute_qemu()
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -04:00
|
|
|
|
elif status == "7":
|
[ADD] déploiement : un proxy SOCKS par SSH, et son mode d'emploi
« -L » relaie UN service ; « -D » ouvre un relais SOCKS, par lequel le
navigateur atteint n'importe quelle destination depuis la machine distante —
une interface qui n'écoute que sur sa boucle locale, un hôte de son réseau sans
route depuis ici. Le port par défaut est 1080, modifiable, et contrôlé libre
avant d'ouvrir. La saisie de l'adresse est celle de sshfs, désormais partagée :
l'alias de ~/.ssh/config part tel quel à ssh, faute de quoi son ProxyJump se
perdrait et une VM imbriquée deviendrait injoignable. Le réglage de Firefox
s'affiche AVANT le lancement, la commande ne rendant la main qu'au Ctrl+C.
Vérifié : 9 tests neufs, et la numérotation du menu que deux autres gardent.
--- EN ---
"-L" relays ONE service; "-D" opens a SOCKS relay, through which the browser
reaches any destination from the remote machine — an interface listening only
on its loopback, a host of its network with no route from here. The default
port is 1080, changeable, and checked free before opening. The address prompt
is sshfs's, now shared: the ~/.ssh/config alias goes to ssh as is, failing
which its ProxyJump would be lost and a nested VM become unreachable. The
Firefox settings print BEFORE the launch, the command only returning on
Ctrl+C.
Checked: 9 new tests, and the menu numbering two others guard.
Assisted-by: Claude Opus 5
2026-09-24 13:32:28 -04:00
|
|
|
|
self.prompt_execute_proxmox()
|
[ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.
Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.
--- EN ---
`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.
The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.
Assisted-by: Claude Opus 5
2026-09-03 23:43:02 -04:00
|
|
|
|
elif status == "8":
|
[ADD] déploiement : un proxy SOCKS par SSH, et son mode d'emploi
« -L » relaie UN service ; « -D » ouvre un relais SOCKS, par lequel le
navigateur atteint n'importe quelle destination depuis la machine distante —
une interface qui n'écoute que sur sa boucle locale, un hôte de son réseau sans
route depuis ici. Le port par défaut est 1080, modifiable, et contrôlé libre
avant d'ouvrir. La saisie de l'adresse est celle de sshfs, désormais partagée :
l'alias de ~/.ssh/config part tel quel à ssh, faute de quoi son ProxyJump se
perdrait et une VM imbriquée deviendrait injoignable. Le réglage de Firefox
s'affiche AVANT le lancement, la commande ne rendant la main qu'au Ctrl+C.
Vérifié : 9 tests neufs, et la numérotation du menu que deux autres gardent.
--- EN ---
"-L" relays ONE service; "-D" opens a SOCKS relay, through which the browser
reaches any destination from the remote machine — an interface listening only
on its loopback, a host of its network with no route from here. The default
port is 1080, changeable, and checked free before opening. The address prompt
is sshfs's, now shared: the ~/.ssh/config alias goes to ssh as is, failing
which its ProxyJump would be lost and a nested VM become unreachable. The
Firefox settings print BEFORE the launch, the command only returning on
Ctrl+C.
Checked: 9 new tests, and the menu numbering two others guard.
Assisted-by: Claude Opus 5
2026-09-24 13:32:28 -04:00
|
|
|
|
self._deploy_ntfy_server()
|
[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 status == "9":
|
[ADD] déploiement : un proxy SOCKS par SSH, et son mode d'emploi
« -L » relaie UN service ; « -D » ouvre un relais SOCKS, par lequel le
navigateur atteint n'importe quelle destination depuis la machine distante —
une interface qui n'écoute que sur sa boucle locale, un hôte de son réseau sans
route depuis ici. Le port par défaut est 1080, modifiable, et contrôlé libre
avant d'ouvrir. La saisie de l'adresse est celle de sshfs, désormais partagée :
l'alias de ~/.ssh/config part tel quel à ssh, faute de quoi son ProxyJump se
perdrait et une VM imbriquée deviendrait injoignable. Le réglage de Firefox
s'affiche AVANT le lancement, la commande ne rendant la main qu'au Ctrl+C.
Vérifié : 9 tests neufs, et la numérotation du menu que deux autres gardent.
--- EN ---
"-L" relays ONE service; "-D" opens a SOCKS relay, through which the browser
reaches any destination from the remote machine — an interface listening only
on its loopback, a host of its network with no route from here. The default
port is 1080, changeable, and checked free before opening. The address prompt
is sshfs's, now shared: the ~/.ssh/config alias goes to ssh as is, failing
which its ProxyJump would be lost and a nested VM become unreachable. The
Firefox settings print BEFORE the launch, the command only returning on
Ctrl+C.
Checked: 9 new tests, and the menu numbering two others guard.
Assisted-by: Claude Opus 5
2026-09-24 13:32:28 -04:00
|
|
|
|
self.prompt_execute_qemu_cache()
|
|
|
|
|
|
elif status == "10":
|
[ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.
Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.
--- EN ---
`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.
The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.
Assisted-by: Claude Opus 5
2026-09-03 23:43:02 -04:00
|
|
|
|
self.prompt_execute_vpn()
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
else:
|
|
|
|
|
|
print(t("Command not found !"))
|
|
|
|
|
|
|
|
|
|
|
|
def prompt_execute_deploy_ssh(self):
|
|
|
|
|
|
"""Sous-menu : opérations de déploiement sur un hôte distant via SSH."""
|
|
|
|
|
|
print(f"🤖 {t('Deploy ERPLibre to a remote host over SSH!')}")
|
|
|
|
|
|
choices = [
|
[ADD] make: add SSH remote deployment targets and todo.py integration
Enable deploying and managing ERPLibre on remote servers via SSH
directly from make and the interactive todo.py CLI, since only
local deployment was previously supported.
- New conf/make.ssh.Makefile with 11 targets: ssh_check, ssh_push,
ssh_install, ssh_run, ssh_stop, ssh_restart, ssh_status, ssh_logs,
ssh_make, ssh_install_systemd, ssh_install_nginx
- Variables: SSH_HOST (required), SSH_USER, SSH_PORT, SSH_KEY,
SSH_PATH, SSH_TARGET, SSH_DOMAIN, SSH_ADMIN_EMAIL
- Execute > Deploy menu extended with 11 SSH options in todo.py
- All strings translated fr/en in todo_i18n.py
Generated by Claude Code 2.1.101 model claude-sonnet-4-6
Co-Authored-By: Mathieu Benoit <mathben@technolibre.ca>
2026-04-10 22:53:07 -04:00
|
|
|
|
{"prompt_description": t("SSH - Check connection")},
|
|
|
|
|
|
{"prompt_description": t("SSH - Sync files (rsync)")},
|
|
|
|
|
|
{"prompt_description": t("SSH - Install ERPLibre")},
|
|
|
|
|
|
{"prompt_description": t("SSH - Start Odoo")},
|
|
|
|
|
|
{"prompt_description": t("SSH - Stop Odoo")},
|
|
|
|
|
|
{"prompt_description": t("SSH - Restart Odoo")},
|
|
|
|
|
|
{"prompt_description": t("SSH - Service status")},
|
|
|
|
|
|
{"prompt_description": t("SSH - View logs")},
|
|
|
|
|
|
{"prompt_description": t("SSH - Run make target")},
|
|
|
|
|
|
{"prompt_description": t("SSH - Install systemd service")},
|
|
|
|
|
|
{"prompt_description": t("SSH - Configure nginx + SSL")},
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
]
|
|
|
|
|
|
help_info = self.fill_help_info(choices)
|
|
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
|
print()
|
|
|
|
|
|
if status == "0":
|
|
|
|
|
|
return False
|
|
|
|
|
|
elif status == "1":
|
|
|
|
|
|
self._deploy_ssh_check()
|
|
|
|
|
|
elif status == "2":
|
|
|
|
|
|
self._deploy_ssh_push()
|
|
|
|
|
|
elif status == "3":
|
|
|
|
|
|
self._deploy_ssh_install()
|
|
|
|
|
|
elif status == "4":
|
|
|
|
|
|
self._deploy_ssh_run()
|
|
|
|
|
|
elif status == "5":
|
|
|
|
|
|
self._deploy_ssh_stop()
|
|
|
|
|
|
elif status == "6":
|
|
|
|
|
|
self._deploy_ssh_restart()
|
|
|
|
|
|
elif status == "7":
|
|
|
|
|
|
self._deploy_ssh_status()
|
|
|
|
|
|
elif status == "8":
|
|
|
|
|
|
self._deploy_ssh_logs()
|
|
|
|
|
|
elif status == "9":
|
|
|
|
|
|
self._deploy_ssh_make()
|
|
|
|
|
|
elif status == "10":
|
|
|
|
|
|
self._deploy_ssh_install_systemd()
|
|
|
|
|
|
elif status == "11":
|
|
|
|
|
|
self._deploy_ssh_install_nginx()
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(t("Command not found !"))
|
|
|
|
|
|
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -04:00
|
|
|
|
@staticmethod
|
[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
|
|
|
|
def _native_arch():
|
|
|
|
|
|
"""Architecture native de l'hôte, en jeton de deploy_qemu.py
|
|
|
|
|
|
(amd64/arm64/s390x). Défaut amd64 si indéterminée."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
machine = os.uname().machine
|
|
|
|
|
|
except (AttributeError, OSError):
|
|
|
|
|
|
machine = ""
|
|
|
|
|
|
return {
|
|
|
|
|
|
"x86_64": "amd64",
|
|
|
|
|
|
"amd64": "amd64",
|
|
|
|
|
|
"aarch64": "arm64",
|
|
|
|
|
|
"arm64": "arm64",
|
|
|
|
|
|
"s390x": "s390x",
|
|
|
|
|
|
}.get(machine, "amd64")
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -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
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _port_in_use(port):
|
|
|
|
|
|
"""Le port est-il déjà pris sur CETTE machine ?
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -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
|
|
|
|
Un second tunnel sur le même port échouerait, et le message d'ssh
|
|
|
|
|
|
(« bind: Address already in use ») se perd en mode détaché."""
|
|
|
|
|
|
with socket.socket() as sock:
|
|
|
|
|
|
sock.settimeout(1)
|
|
|
|
|
|
return sock.connect_ex(("127.0.0.1", port)) == 0
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -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
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _ssh_proxyjump(host):
|
|
|
|
|
|
"""ProxyJump effectif d'un hôte, tel que ssh le calcule lui-même.
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -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
|
|
|
|
« ssh -G » rend la configuration RÉSOLUE : Match, wildcards et Include
|
|
|
|
|
|
compris. Relire ~/.ssh/config à la main raterait tout cela.
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -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
|
|
|
|
try:
|
|
|
|
|
|
res = subprocess.run(
|
|
|
|
|
|
["ssh", "-G", host], capture_output=True, text=True, timeout=10
|
|
|
|
|
|
)
|
|
|
|
|
|
except (OSError, subprocess.SubprocessError):
|
|
|
|
|
|
return ""
|
|
|
|
|
|
for line in res.stdout.splitlines():
|
|
|
|
|
|
if line.startswith("proxyjump "):
|
|
|
|
|
|
value = line.split(None, 1)[1].strip()
|
|
|
|
|
|
return "" if value.lower() == "none" else value
|
|
|
|
|
|
return ""
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -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
|
|
|
|
def _ssh_ensure_key(self):
|
|
|
|
|
|
"""Chemin de la clé PUBLIQUE, générée si aucune n'existe.
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -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
|
|
|
|
Sans clé, ssh-copy-id n'a rien à déployer. On en crée une ed25519 sans
|
|
|
|
|
|
passphrase — le même choix que `deploy_qemu.ensure_ssh_key`, pour que
|
|
|
|
|
|
les VM créées et celles adoptées ici partagent la même clé."""
|
|
|
|
|
|
existing = self._qemu_default_ssh_key()
|
|
|
|
|
|
if existing:
|
|
|
|
|
|
return existing
|
|
|
|
|
|
path = os.path.expanduser("~/.ssh/id_ed25519")
|
|
|
|
|
|
os.makedirs(os.path.dirname(path), mode=0o700, exist_ok=True)
|
|
|
|
|
|
print(f"🔑 {t('Generating an ed25519 SSH key')}: {path}")
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -04:00
|
|
|
|
try:
|
|
|
|
|
|
res = subprocess.run(
|
[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
|
|
|
|
["ssh-keygen", "-t", "ed25519", "-N", "", "-f", path],
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -04:00
|
|
|
|
capture_output=True,
|
|
|
|
|
|
text=True,
|
[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
|
|
|
|
timeout=60,
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -04:00
|
|
|
|
)
|
|
|
|
|
|
except (OSError, subprocess.SubprocessError) as exc:
|
[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
|
|
|
|
print(f" ⚠ {t('Cannot generate the key')}: {exc}")
|
|
|
|
|
|
return ""
|
|
|
|
|
|
if res.returncode != 0:
|
|
|
|
|
|
print(f" ⚠ {t('Cannot generate the key')}: {res.stderr.strip()}")
|
|
|
|
|
|
return ""
|
|
|
|
|
|
return f"{path}.pub"
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -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
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _ssh_key_accepted(alias):
|
|
|
|
|
|
"""Vrai si la connexion par CLÉ passe déjà (aucun mot de passe).
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -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
|
|
|
|
`PasswordAuthentication=no` est le point clé : sans lui, ssh
|
|
|
|
|
|
basculerait sur le mot de passe et on croirait la clé installée."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
res = subprocess.run(
|
|
|
|
|
|
[
|
|
|
|
|
|
"ssh",
|
|
|
|
|
|
"-o",
|
|
|
|
|
|
"BatchMode=yes",
|
|
|
|
|
|
"-o",
|
|
|
|
|
|
"PasswordAuthentication=no",
|
|
|
|
|
|
"-o",
|
|
|
|
|
|
"ConnectTimeout=10",
|
|
|
|
|
|
alias,
|
|
|
|
|
|
"true",
|
|
|
|
|
|
],
|
|
|
|
|
|
capture_output=True,
|
|
|
|
|
|
timeout=45,
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -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
|
|
|
|
return res.returncode == 0
|
|
|
|
|
|
except (OSError, subprocess.SubprocessError):
|
|
|
|
|
|
return False
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -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
|
|
|
|
def _ssh_deploy_keys(self, aliases):
|
|
|
|
|
|
"""Déploie la clé publique sur les hôtes qui ne l'ont pas encore.
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -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
|
|
|
|
ssh-copy-id passe par ssh, donc par ~/.ssh/config : le ProxyJump d'une
|
|
|
|
|
|
VM imbriquée s'applique tout seul. Le mot de passe est demandé
|
|
|
|
|
|
directement dans le terminal (pas de capture de la sortie), sinon
|
|
|
|
|
|
l'invite serait invisible."""
|
|
|
|
|
|
if not aliases:
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -04:00
|
|
|
|
return
|
[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
|
|
|
|
pub = self._ssh_ensure_key()
|
|
|
|
|
|
if not pub:
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -04:00
|
|
|
|
return
|
[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
|
|
|
|
print(
|
|
|
|
|
|
f"\n🔑 {t('Deploying the key on')} {len(aliases)} "
|
|
|
|
|
|
f"{self._plural(t('host'), len(aliases))} ({pub})"
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -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
|
|
|
|
n_ok = n_skip = n_fail = 0
|
|
|
|
|
|
for alias in aliases:
|
|
|
|
|
|
if self._ssh_key_accepted(alias):
|
|
|
|
|
|
print(f" · {alias}: {t('key already accepted')}")
|
|
|
|
|
|
n_skip += 1
|
|
|
|
|
|
continue
|
|
|
|
|
|
print(f" ⤴ ssh-copy-id {alias}")
|
|
|
|
|
|
try:
|
|
|
|
|
|
res = subprocess.run(
|
|
|
|
|
|
["ssh-copy-id", "-i", pub, alias], timeout=180
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -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
|
|
|
|
ok = res.returncode == 0
|
|
|
|
|
|
except (OSError, subprocess.SubprocessError) as exc:
|
|
|
|
|
|
print(f" ⚠ {exc}")
|
|
|
|
|
|
ok = False
|
|
|
|
|
|
if ok:
|
|
|
|
|
|
n_ok += 1
|
|
|
|
|
|
else:
|
|
|
|
|
|
n_fail += 1
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -04:00
|
|
|
|
print(
|
[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
|
|
|
|
f" {n_ok} {t('deployed')} · {n_skip} {t('already there')} · "
|
|
|
|
|
|
f"{n_fail} {t('failed')}"
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -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
|
|
|
|
# Connexions de virt-manager : stockées dans GSettings, pas dans un
|
|
|
|
|
|
# fichier. Le schéma est le même depuis des années (virt-manager 5.1
|
|
|
|
|
|
# inclus) ; on LIT d'abord, et on n'écrit que si la lecture a marché.
|
|
|
|
|
|
_VIRT_MANAGER_SCHEMA = "org.virt-manager.virt-manager.connections"
|
|
|
|
|
|
# Schéma RELOCATABLE d'UNE connexion : il porte son nom affiché.
|
|
|
|
|
|
_VIRT_MANAGER_CONN_SCHEMA = "org.virt-manager.virt-manager.connection"
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -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
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _virt_manager_conn_path(uri):
|
|
|
|
|
|
"""Chemin dconf des réglages d'une connexion.
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -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
|
|
|
|
virt-manager ne fait aucun échappement : il retire simplement TOUS
|
|
|
|
|
|
les « / » de l'URI et s'en sert comme segment unique
|
|
|
|
|
|
(virtManager/config.py, _make_perconn_key). Le « :», le « @» et le
|
|
|
|
|
|
« + » restent donc tels quels."""
|
|
|
|
|
|
return f"/org/virt-manager/virt-manager/conns/{uri.replace('/', '')}/"
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -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
|
|
|
|
def _virt_manager_set_label(self, uri, label):
|
|
|
|
|
|
"""Fixe le nom affiché d'une connexion. Sans lui, virt-manager
|
|
|
|
|
|
fabrique un libellé à partir de l'URI, où l'imbrication se lit mal."""
|
|
|
|
|
|
target = (
|
|
|
|
|
|
f"{self._VIRT_MANAGER_CONN_SCHEMA}:"
|
|
|
|
|
|
f"{self._virt_manager_conn_path(uri)}"
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -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
|
|
|
|
try:
|
|
|
|
|
|
res = subprocess.run(
|
|
|
|
|
|
["gsettings", "set", target, "pretty-name", label],
|
|
|
|
|
|
capture_output=True,
|
|
|
|
|
|
text=True,
|
|
|
|
|
|
timeout=15,
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -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
|
|
|
|
except (OSError, subprocess.SubprocessError):
|
|
|
|
|
|
return False
|
|
|
|
|
|
return res.returncode == 0
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -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
|
|
|
|
def _virt_manager_uris(self):
|
|
|
|
|
|
"""URI déjà connues de virt-manager, ou None s'il n'est pas là."""
|
|
|
|
|
|
if not shutil.which("virt-manager") or not shutil.which("gsettings"):
|
|
|
|
|
|
return None
|
|
|
|
|
|
try:
|
|
|
|
|
|
res = subprocess.run(
|
|
|
|
|
|
["gsettings", "get", self._VIRT_MANAGER_SCHEMA, "uris"],
|
|
|
|
|
|
capture_output=True,
|
|
|
|
|
|
text=True,
|
|
|
|
|
|
timeout=15,
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -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
|
|
|
|
except (OSError, subprocess.SubprocessError):
|
|
|
|
|
|
return None
|
|
|
|
|
|
if res.returncode != 0:
|
|
|
|
|
|
return None
|
|
|
|
|
|
# GSettings rend du littéral Python : ['a', 'b'] ou @as [].
|
|
|
|
|
|
raw = res.stdout.strip()
|
|
|
|
|
|
if raw.startswith("@as "):
|
|
|
|
|
|
raw = raw[4:].strip()
|
[IMP] qemu: a VM one can reach, and a first boot that does not stall
The monitor froze the address given at launch, so it lost the VM as soon as
cloud-init renamed the host and DHCP handed out another lease. It now
re-resolves at each attempt, in the views too, and reads virsh without sudo
— « sudo -n » fails in a detached session with no tty.
First boot also stopped paying for what it does not need: the guest agent
leaves cloud-init, snapd and locale-gen go, apt takes the fastest mirror.
The timezone follows the host. And when KVM is missing, the deployment says
so before the wait instead of being mysteriously fifteen times slower.
--- FR ---
Le suivi figeait l'adresse connue au lancement : il perdait donc la VM dès
que cloud-init posait le vrai nom d'hôte et que DHCP donnait un autre bail.
Il la ré-résout désormais à chaque tentative, dans les vues aussi, et lit
virsh sans sudo — « sudo -n » échoue dans une session détachée, sans tty.
Le premier démarrage cesse aussi de payer l'inutile : l'agent invité sort
de cloud-init, snapd et locale-gen disparaissent, apt prend le miroir le
plus rapide. Le fuseau suit l'hôte. Et faute de KVM, le déploiement le dit
avant l'attente, au lieu d'être quinze fois plus lent sans raison visible.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
try:
|
[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
|
|
|
|
value = ast.literal_eval(raw)
|
|
|
|
|
|
except (ValueError, SyntaxError):
|
|
|
|
|
|
return None
|
|
|
|
|
|
return [str(item) for item in value] if isinstance(value, list) else []
|
[IMP] qemu: a VM one can reach, and a first boot that does not stall
The monitor froze the address given at launch, so it lost the VM as soon as
cloud-init renamed the host and DHCP handed out another lease. It now
re-resolves at each attempt, in the views too, and reads virsh without sudo
— « sudo -n » fails in a detached session with no tty.
First boot also stopped paying for what it does not need: the guest agent
leaves cloud-init, snapd and locale-gen go, apt takes the fastest mirror.
The timezone follows the host. And when KVM is missing, the deployment says
so before the wait instead of being mysteriously fifteen times slower.
--- FR ---
Le suivi figeait l'adresse connue au lancement : il perdait donc la VM dès
que cloud-init posait le vrai nom d'hôte et que DHCP donnait un autre bail.
Il la ré-résout désormais à chaque tentative, dans les vues aussi, et lit
virsh sans sudo — « sudo -n » échoue dans une session détachée, sans tty.
Le premier démarrage cesse aussi de payer l'inutile : l'agent invité sort
de cloud-init, snapd et locale-gen disparaissent, apt prend le miroir le
plus rapide. Le fuseau suit l'hôte. Et faute de KVM, le déploiement le dit
avant l'attente, au lieu d'être quinze fois plus lent sans raison visible.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -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
|
|
|
|
def _virt_manager_add(self, uris):
|
|
|
|
|
|
"""Ajoute les URI manquantes à virt-manager. Renvoie le nb ajouté."""
|
|
|
|
|
|
current = self._virt_manager_uris()
|
|
|
|
|
|
if current is None:
|
|
|
|
|
|
return 0
|
|
|
|
|
|
missing = [uri for uri in uris if uri not in current]
|
|
|
|
|
|
if not missing:
|
|
|
|
|
|
print(f" · {t('virt-manager: every connection already there')}")
|
|
|
|
|
|
return 0
|
|
|
|
|
|
merged = current + missing
|
|
|
|
|
|
literal = "[" + ", ".join(f"'{uri}'" for uri in merged) + "]"
|
|
|
|
|
|
try:
|
|
|
|
|
|
res = subprocess.run(
|
|
|
|
|
|
[
|
|
|
|
|
|
"gsettings",
|
|
|
|
|
|
"set",
|
|
|
|
|
|
self._VIRT_MANAGER_SCHEMA,
|
|
|
|
|
|
"uris",
|
|
|
|
|
|
literal,
|
|
|
|
|
|
],
|
|
|
|
|
|
capture_output=True,
|
|
|
|
|
|
text=True,
|
|
|
|
|
|
timeout=15,
|
[IMP] qemu: a VM one can reach, and a first boot that does not stall
The monitor froze the address given at launch, so it lost the VM as soon as
cloud-init renamed the host and DHCP handed out another lease. It now
re-resolves at each attempt, in the views too, and reads virsh without sudo
— « sudo -n » fails in a detached session with no tty.
First boot also stopped paying for what it does not need: the guest agent
leaves cloud-init, snapd and locale-gen go, apt takes the fastest mirror.
The timezone follows the host. And when KVM is missing, the deployment says
so before the wait instead of being mysteriously fifteen times slower.
--- FR ---
Le suivi figeait l'adresse connue au lancement : il perdait donc la VM dès
que cloud-init posait le vrai nom d'hôte et que DHCP donnait un autre bail.
Il la ré-résout désormais à chaque tentative, dans les vues aussi, et lit
virsh sans sudo — « sudo -n » échoue dans une session détachée, sans tty.
Le premier démarrage cesse aussi de payer l'inutile : l'agent invité sort
de cloud-init, snapd et locale-gen disparaissent, apt prend le miroir le
plus rapide. Le fuseau suit l'hôte. Et faute de KVM, le déploiement le dit
avant l'attente, au lieu d'être quinze fois plus lent sans raison visible.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -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
|
|
|
|
except (OSError, subprocess.SubprocessError) as exc:
|
|
|
|
|
|
print(f" ⚠ virt-manager: {exc}")
|
|
|
|
|
|
return 0
|
|
|
|
|
|
if res.returncode != 0:
|
|
|
|
|
|
print(f" ⚠ virt-manager: {res.stderr.strip()}")
|
|
|
|
|
|
return 0
|
|
|
|
|
|
for uri in missing:
|
|
|
|
|
|
print(f" ✅ virt-manager: {uri}")
|
|
|
|
|
|
return len(missing)
|
[IMP] qemu: a VM one can reach, and a first boot that does not stall
The monitor froze the address given at launch, so it lost the VM as soon as
cloud-init renamed the host and DHCP handed out another lease. It now
re-resolves at each attempt, in the views too, and reads virsh without sudo
— « sudo -n » fails in a detached session with no tty.
First boot also stopped paying for what it does not need: the guest agent
leaves cloud-init, snapd and locale-gen go, apt takes the fastest mirror.
The timezone follows the host. And when KVM is missing, the deployment says
so before the wait instead of being mysteriously fifteen times slower.
--- FR ---
Le suivi figeait l'adresse connue au lancement : il perdait donc la VM dès
que cloud-init posait le vrai nom d'hôte et que DHCP donnait un autre bail.
Il la ré-résout désormais à chaque tentative, dans les vues aussi, et lit
virsh sans sudo — « sudo -n » échoue dans une session détachée, sans tty.
Le premier démarrage cesse aussi de payer l'inutile : l'agent invité sort
de cloud-init, snapd et locale-gen disparaissent, apt prend le miroir le
plus rapide. Le fuseau suit l'hôte. Et faute de KVM, le déploiement le dit
avant l'attente, au lieu d'être quinze fois plus lent sans raison visible.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -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
|
|
|
|
def _virt_manager_offer(self, hosts):
|
|
|
|
|
|
"""Propose d'ajouter à virt-manager les machines qui font tourner
|
|
|
|
|
|
libvirt, pour piloter leurs VM depuis l'interface graphique locale.
|
[IMP] qemu: a VM one can reach, and a first boot that does not stall
The monitor froze the address given at launch, so it lost the VM as soon as
cloud-init renamed the host and DHCP handed out another lease. It now
re-resolves at each attempt, in the views too, and reads virsh without sudo
— « sudo -n » fails in a detached session with no tty.
First boot also stopped paying for what it does not need: the guest agent
leaves cloud-init, snapd and locale-gen go, apt takes the fastest mirror.
The timezone follows the host. And when KVM is missing, the deployment says
so before the wait instead of being mysteriously fifteen times slower.
--- FR ---
Le suivi figeait l'adresse connue au lancement : il perdait donc la VM dès
que cloud-init posait le vrai nom d'hôte et que DHCP donnait un autre bail.
Il la ré-résout désormais à chaque tentative, dans les vues aussi, et lit
virsh sans sudo — « sudo -n » échoue dans une session détachée, sans tty.
Le premier démarrage cesse aussi de payer l'inutile : l'agent invité sort
de cloud-init, snapd et locale-gen disparaissent, apt prend le miroir le
plus rapide. Le fuseau suit l'hôte. Et faute de KVM, le déploiement le dit
avant l'attente, au lieu d'être quinze fois plus lent sans raison visible.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -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
|
|
|
|
On passe par l'ALIAS SSH et non par l'IP : le transport qemu+ssh
|
|
|
|
|
|
utilise le binaire ssh, donc ~/.ssh/config — l'alias porte déjà
|
|
|
|
|
|
l'adresse ET le ProxyJump, ce qu'une IP brute ne saurait pas faire
|
|
|
|
|
|
pour une VM imbriquée. `hosts` = [(alias_chaîné, compte)], de sorte
|
|
|
|
|
|
que l'imbrication se lise aussi dans l'interface graphique et que le
|
|
|
|
|
|
compte soit celui de ~/.ssh/config, pas un défaut supposé."""
|
|
|
|
|
|
if self._virt_manager_uris() is None:
|
[IMP] qemu: a VM one can reach, and a first boot that does not stall
The monitor froze the address given at launch, so it lost the VM as soon as
cloud-init renamed the host and DHCP handed out another lease. It now
re-resolves at each attempt, in the views too, and reads virsh without sudo
— « sudo -n » fails in a detached session with no tty.
First boot also stopped paying for what it does not need: the guest agent
leaves cloud-init, snapd and locale-gen go, apt takes the fastest mirror.
The timezone follows the host. And when KVM is missing, the deployment says
so before the wait instead of being mysteriously fifteen times slower.
--- FR ---
Le suivi figeait l'adresse connue au lancement : il perdait donc la VM dès
que cloud-init posait le vrai nom d'hôte et que DHCP donnait un autre bail.
Il la ré-résout désormais à chaque tentative, dans les vues aussi, et lit
virsh sans sudo — « sudo -n » échoue dans une session détachée, sans tty.
Le premier démarrage cesse aussi de payer l'inutile : l'agent invité sort
de cloud-init, snapd et locale-gen disparaissent, apt prend le miroir le
plus rapide. Le fuseau suit l'hôte. Et faute de KVM, le déploiement le dit
avant l'attente, au lieu d'être quinze fois plus lent sans raison visible.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
return
|
[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
|
|
|
|
labels = {
|
|
|
|
|
|
f"qemu+ssh://{user or self.QEMU_VM_USER}@{alias}/system": alias
|
|
|
|
|
|
for alias, user in hosts
|
[IMP] qemu: a VM one can reach, and a first boot that does not stall
The monitor froze the address given at launch, so it lost the VM as soon as
cloud-init renamed the host and DHCP handed out another lease. It now
re-resolves at each attempt, in the views too, and reads virsh without sudo
— « sudo -n » fails in a detached session with no tty.
First boot also stopped paying for what it does not need: the guest agent
leaves cloud-init, snapd and locale-gen go, apt takes the fastest mirror.
The timezone follows the host. And when KVM is missing, the deployment says
so before the wait instead of being mysteriously fifteen times slower.
--- FR ---
Le suivi figeait l'adresse connue au lancement : il perdait donc la VM dès
que cloud-init posait le vrai nom d'hôte et que DHCP donnait un autre bail.
Il la ré-résout désormais à chaque tentative, dans les vues aussi, et lit
virsh sans sudo — « sudo -n » échoue dans une session détachée, sans tty.
Le premier démarrage cesse aussi de payer l'inutile : l'agent invité sort
de cloud-init, snapd et locale-gen disparaissent, apt prend le miroir le
plus rapide. Le fuseau suit l'hôte. Et faute de KVM, le déploiement le dit
avant l'attente, au lieu d'être quinze fois plus lent sans raison visible.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -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
|
|
|
|
uris = ["qemu:///system"] + list(labels)
|
|
|
|
|
|
print(f"\n🖥 {t('virt-manager detected')}")
|
|
|
|
|
|
for uri in uris:
|
|
|
|
|
|
print(f" {uri}")
|
|
|
|
|
|
if not self._is_yes_default_yes(
|
|
|
|
|
|
input(t("Add the missing connections to virt-manager? (Y/n): "))
|
|
|
|
|
|
):
|
[IMP] qemu: a VM one can reach, and a first boot that does not stall
The monitor froze the address given at launch, so it lost the VM as soon as
cloud-init renamed the host and DHCP handed out another lease. It now
re-resolves at each attempt, in the views too, and reads virsh without sudo
— « sudo -n » fails in a detached session with no tty.
First boot also stopped paying for what it does not need: the guest agent
leaves cloud-init, snapd and locale-gen go, apt takes the fastest mirror.
The timezone follows the host. And when KVM is missing, the deployment says
so before the wait instead of being mysteriously fifteen times slower.
--- FR ---
Le suivi figeait l'adresse connue au lancement : il perdait donc la VM dès
que cloud-init posait le vrai nom d'hôte et que DHCP donnait un autre bail.
Il la ré-résout désormais à chaque tentative, dans les vues aussi, et lit
virsh sans sudo — « sudo -n » échoue dans une session détachée, sans tty.
Le premier démarrage cesse aussi de payer l'inutile : l'agent invité sort
de cloud-init, snapd et locale-gen disparaissent, apt prend le miroir le
plus rapide. Le fuseau suit l'hôte. Et faute de KVM, le déploiement le dit
avant l'attente, au lieu d'être quinze fois plus lent sans raison visible.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
return
|
[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 nom affiché est posé AVANT l'ajout à la liste : virt-manager le
|
|
|
|
|
|
# lit au moment d'afficher la connexion, pas à l'inscription.
|
|
|
|
|
|
for uri, label in labels.items():
|
|
|
|
|
|
self._virt_manager_set_label(uri, label)
|
|
|
|
|
|
added = self._virt_manager_add(uris)
|
|
|
|
|
|
if added:
|
|
|
|
|
|
# Le NOM est relu à chaud (virt-manager écoute /pretty-name),
|
|
|
|
|
|
# mais la liste des connexions est lue au démarrage : une
|
|
|
|
|
|
# nouvelle entrée n'apparaît qu'au prochain lancement.
|
|
|
|
|
|
note = t("Restart virt-manager to see the new connections")
|
|
|
|
|
|
print(f" ℹ️ {note} ({t('the names apply live')})")
|
[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
|
|
|
|
|
[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
|
|
|
|
# Signatures d'un refus d'AUTHENTIFICATION dans la sortie de ssh, par
|
|
|
|
|
|
# opposition à un hôte éteint ou introuvable. C'est la distinction qui
|
|
|
|
|
|
# décide s'il vaut la peine de parler de clé SSH.
|
|
|
|
|
|
_SSH_AUTH_ERRORS = (
|
|
|
|
|
|
"permission denied",
|
|
|
|
|
|
"too many authentication failures",
|
|
|
|
|
|
"no such identity",
|
|
|
|
|
|
"host key verification failed",
|
|
|
|
|
|
"publickey",
|
|
|
|
|
|
)
|
[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
|
|
|
|
|
[ADD] qemu: choose the application store of a graphical VM
snap was not a choice but a fate: we disabled snapd, then gnome-core
pulled a Firefox snap that froze the install for thirty minutes. The
previous fix imposed "deb". Three answers are now offered.
deb nothing but .deb, epiphany-browser as the browser. Default,
and lightest: nothing extra to download.
flatpak Flatpak tooling as well, WITHOUT the Flathub remote or any
install. The machine is ready, the admin picks its remotes.
snap Ubuntu's default, snapd left running and Firefox as a snap.
It is the only mode where snapd is not disabled -- disabling
it was precisely the cause of the freeze.
The question is only asked when it means something: at least one
graphical VM on a distro shipping snapd, Ubuntu alone here. A server
pulls no snap, and Debian ships none. The TUI greys the choice out and
says why; the CLI does not ask.
Verified on a 26.04 VM: snapd is indeed preinstalled (2.75.2), flatpak
and its GNOME Software plugin are packaged, and firefox-esr does not
exist on Ubuntu.
--- FR ---
snap n'était plus un choix mais une fatalité : on coupait snapd, puis
gnome-core tirait un Firefox-snap qui figeait l'installation trente
minutes. Le correctif précédent imposait « deb ». Trois réponses sont
maintenant offertes.
deb rien que des .deb, epiphany-browser comme navigateur. Défaut,
et le plus léger : rien de plus à télécharger.
flatpak l'outillage Flatpak en plus, SANS dépôt Flathub ni
installation. La machine est prête, l'administrateur choisit
ses dépôts.
snap le défaut d'Ubuntu, snapd laissé actif et Firefox en snap.
C'est le seul mode où snapd n'est pas coupé — l'y couper
était précisément la cause du blocage.
La question n'est posée que lorsqu'elle a un sens : au moins une VM
graphique sur une distribution qui livre snapd, Ubuntu seule ici. Un
serveur ne tire aucun snap, et Debian n'en livre pas. La TUI grise le
choix et dit pourquoi ; la CLI ne le demande pas.
Vérifié sur une VM 26.04 : snapd y est bien préinstallé (2.75.2),
flatpak et son greffon GNOME Software sont empaquetés, et firefox-esr
n'existe pas sur Ubuntu.
Assisted-by: Claude Opus 5
2026-08-12 03:24:19 -04:00
|
|
|
|
@classmethod
|
[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
|
|
|
|
def _ssh_error_kind(cls, stderr):
|
|
|
|
|
|
"""« auth » si ssh a refusé l'identité, « net » sinon.
|
[ADD] qemu: choose the application store of a graphical VM
snap was not a choice but a fate: we disabled snapd, then gnome-core
pulled a Firefox snap that froze the install for thirty minutes. The
previous fix imposed "deb". Three answers are now offered.
deb nothing but .deb, epiphany-browser as the browser. Default,
and lightest: nothing extra to download.
flatpak Flatpak tooling as well, WITHOUT the Flathub remote or any
install. The machine is ready, the admin picks its remotes.
snap Ubuntu's default, snapd left running and Firefox as a snap.
It is the only mode where snapd is not disabled -- disabling
it was precisely the cause of the freeze.
The question is only asked when it means something: at least one
graphical VM on a distro shipping snapd, Ubuntu alone here. A server
pulls no snap, and Debian ships none. The TUI greys the choice out and
says why; the CLI does not ask.
Verified on a 26.04 VM: snapd is indeed preinstalled (2.75.2), flatpak
and its GNOME Software plugin are packaged, and firefox-esr does not
exist on Ubuntu.
--- FR ---
snap n'était plus un choix mais une fatalité : on coupait snapd, puis
gnome-core tirait un Firefox-snap qui figeait l'installation trente
minutes. Le correctif précédent imposait « deb ». Trois réponses sont
maintenant offertes.
deb rien que des .deb, epiphany-browser comme navigateur. Défaut,
et le plus léger : rien de plus à télécharger.
flatpak l'outillage Flatpak en plus, SANS dépôt Flathub ni
installation. La machine est prête, l'administrateur choisit
ses dépôts.
snap le défaut d'Ubuntu, snapd laissé actif et Firefox en snap.
C'est le seul mode où snapd n'est pas coupé — l'y couper
était précisément la cause du blocage.
La question n'est posée que lorsqu'elle a un sens : au moins une VM
graphique sur une distribution qui livre snapd, Ubuntu seule ici. Un
serveur ne tire aucun snap, et Debian n'en livre pas. La TUI grise le
choix et dit pourquoi ; la CLI ne le demande pas.
Vérifié sur une VM 26.04 : snapd y est bien préinstallé (2.75.2),
flatpak et son greffon GNOME Software sont empaquetés, et firefox-esr
n'existe pas sur Ubuntu.
Assisted-by: Claude Opus 5
2026-08-12 03:24:19 -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
|
|
|
|
Un hôte éteint et une clé absente produisent tous deux « injoignable »
|
|
|
|
|
|
alors qu'ils n'appellent pas du tout la même réponse."""
|
|
|
|
|
|
text = (stderr or "").lower()
|
|
|
|
|
|
if any(marker in text for marker in cls._SSH_AUTH_ERRORS):
|
|
|
|
|
|
return "auth"
|
|
|
|
|
|
return "net"
|
[ADD] qemu: choose the application store of a graphical VM
snap was not a choice but a fate: we disabled snapd, then gnome-core
pulled a Firefox snap that froze the install for thirty minutes. The
previous fix imposed "deb". Three answers are now offered.
deb nothing but .deb, epiphany-browser as the browser. Default,
and lightest: nothing extra to download.
flatpak Flatpak tooling as well, WITHOUT the Flathub remote or any
install. The machine is ready, the admin picks its remotes.
snap Ubuntu's default, snapd left running and Firefox as a snap.
It is the only mode where snapd is not disabled -- disabling
it was precisely the cause of the freeze.
The question is only asked when it means something: at least one
graphical VM on a distro shipping snapd, Ubuntu alone here. A server
pulls no snap, and Debian ships none. The TUI greys the choice out and
says why; the CLI does not ask.
Verified on a 26.04 VM: snapd is indeed preinstalled (2.75.2), flatpak
and its GNOME Software plugin are packaged, and firefox-esr does not
exist on Ubuntu.
--- FR ---
snap n'était plus un choix mais une fatalité : on coupait snapd, puis
gnome-core tirait un Firefox-snap qui figeait l'installation trente
minutes. Le correctif précédent imposait « deb ». Trois réponses sont
maintenant offertes.
deb rien que des .deb, epiphany-browser comme navigateur. Défaut,
et le plus léger : rien de plus à télécharger.
flatpak l'outillage Flatpak en plus, SANS dépôt Flathub ni
installation. La machine est prête, l'administrateur choisit
ses dépôts.
snap le défaut d'Ubuntu, snapd laissé actif et Firefox en snap.
C'est le seul mode où snapd n'est pas coupé — l'y couper
était précisément la cause du blocage.
La question n'est posée que lorsqu'elle a un sens : au moins une VM
graphique sur une distribution qui livre snapd, Ubuntu seule ici. Un
serveur ne tire aucun snap, et Debian n'en livre pas. La TUI grise le
choix et dit pourquoi ; la CLI ne le demande pas.
Vérifié sur une VM 26.04 : snapd y est bien préinstallé (2.75.2),
flatpak et son greffon GNOME Software sont empaquetés, et firefox-esr
n'existe pas sur Ubuntu.
Assisted-by: Claude Opus 5
2026-08-12 03:24:19 -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
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _fmt_uptime(secs):
|
|
|
|
|
|
"""Durée depuis le démarrage, en six caractères au plus.
|
[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
|
|
|
|
|
[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
|
|
|
|
« _fmt_dur » s'arrête aux minutes — bon pour une installation, illisible
|
|
|
|
|
|
pour une VM debout depuis trois jours. Ici la précision décroît avec la
|
|
|
|
|
|
durée : personne ne lit les secondes d'un uptime de 19 heures."""
|
|
|
|
|
|
secs = int(secs)
|
|
|
|
|
|
if secs < 60:
|
|
|
|
|
|
return f"{secs}s"
|
|
|
|
|
|
if secs < 3600:
|
|
|
|
|
|
return f"{secs // 60}m"
|
|
|
|
|
|
if secs < 86400:
|
|
|
|
|
|
return f"{secs // 3600}h{(secs % 3600) // 60:02d}"
|
|
|
|
|
|
days = secs // 86400
|
|
|
|
|
|
# Au-delà de 99 jours, les heures ne rentrent plus dans la colonne — et
|
|
|
|
|
|
# personne ne les lit sur une machine debout depuis un an.
|
|
|
|
|
|
if days >= 100:
|
|
|
|
|
|
return f"{days}j"
|
|
|
|
|
|
return f"{days}j{(secs % 86400) // 3600:02d}h"
|
[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
|
|
|
|
|
[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
|
|
|
|
# Outils requis pour la réduction sûre (tous « base » : e2fsprogs, gdisk,
|
|
|
|
|
|
# util-linux, qemu-utils) — PAS libguestfs (souvent cassé : appliance
|
|
|
|
|
|
# supermin sans noyau dans /boot).
|
|
|
|
|
|
_SHRINK_TOOLS = (
|
|
|
|
|
|
"qemu-nbd",
|
|
|
|
|
|
"e2fsck",
|
|
|
|
|
|
"resize2fs",
|
|
|
|
|
|
"sgdisk",
|
|
|
|
|
|
"partprobe",
|
|
|
|
|
|
"lsblk",
|
|
|
|
|
|
"dumpe2fs",
|
|
|
|
|
|
"blockdev",
|
|
|
|
|
|
)
|
|
|
|
|
|
_SECT = 512
|
|
|
|
|
|
_MiB = 1024 * 1024
|
[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
|
|
|
|
|
[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
|
|
|
|
# Commande d'extension du FS racine (partition + FS) réutilisée par SSH
|
|
|
|
|
|
# et par le repli console série.
|
|
|
|
|
|
_GROW_FS_REMOTE = (
|
|
|
|
|
|
"set -e; "
|
|
|
|
|
|
"root=$(findmnt -no SOURCE /); "
|
|
|
|
|
|
'dev=$(lsblk -no PKNAME "$root" | head -1); '
|
|
|
|
|
|
"part=$(echo \"$root\" | grep -oE '[0-9]+$'); "
|
|
|
|
|
|
"sudo growpart /dev/$dev $part || true; "
|
|
|
|
|
|
"fstype=$(findmnt -no FSTYPE /); "
|
|
|
|
|
|
'case "$fstype" in '
|
|
|
|
|
|
'ext*) sudo resize2fs "$root";; '
|
|
|
|
|
|
"xfs) sudo xfs_growfs /;; "
|
|
|
|
|
|
"btrfs) sudo btrfs filesystem resize max /;; "
|
|
|
|
|
|
"esac; "
|
|
|
|
|
|
"df -h /"
|
|
|
|
|
|
)
|
[ADD] tui qemu: choose mise or pyenv at deploy time
The interpreter provider is now chosen when creating a VM, in both
interfaces. "mise" installs it in the VM and lays down a precompiled
CPython; "pyenv" keeps today's build-from-source.
The question is only asked where it means something: mise publishes
binaries for amd64 and arm64 only. An all-s390x fleet never sees it, a
mixed fleet sees it along with the architectures that will fall back to
pyenv -- said before deploying, not discovered in a log an hour later.
mise goes into /usr/local/bin rather than ~/.local/bin: the remote command
runs through "ssh host 'command'", where neither ~/.profile nor ~/.bashrc
is read. Choosing "mise" exports "auto", not "mise", so a failed install
can still fall back to pyenv.
The unavailability notice was translated as "pyenv required", which said
the opposite of what happens: pyenv is the fallback, not a demand.
--- FR ---
Le fournisseur d'interpréteur se choisit désormais à la création d'une VM,
dans les deux interfaces. « mise » l'installe dans la VM et pose un
CPython précompilé ; « pyenv » garde la compilation d'aujourd'hui.
La question n'est posée que là où elle a un sens : mise ne publie de
binaires que pour amd64 et arm64. Un parc tout s390x ne la voit jamais, un
parc mixte la voit avec les architectures qui retomberont sur pyenv — dit
avant de déployer, pas découvert dans un log une heure plus tard.
mise va dans /usr/local/bin plutôt que ~/.local/bin : la commande distante
passe par « ssh hôte 'commande' », où ni ~/.profile ni ~/.bashrc ne sont
lus. Choisir « mise » exporte « auto », pas « mise », pour qu'une
installation ratée puisse encore retomber sur pyenv.
L'avis d'indisponibilité était traduit par « pyenv exigé », qui disait
l'inverse de ce qui se passe : pyenv est le repli, pas une exigence.
Assisted-by: Claude Opus 5
2026-08-11 22:11:23 -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
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _ssh_private_key(pub_path):
|
|
|
|
|
|
"""Clé PRIVÉE correspondant à une clé publique, ou '' si introuvable.
|
|
|
|
|
|
C'est elle que réclame IdentityFile ; donner le « .pub » ferait
|
|
|
|
|
|
échouer l'authentification."""
|
|
|
|
|
|
if not pub_path:
|
[ADD] tui qemu: choose mise or pyenv at deploy time
The interpreter provider is now chosen when creating a VM, in both
interfaces. "mise" installs it in the VM and lays down a precompiled
CPython; "pyenv" keeps today's build-from-source.
The question is only asked where it means something: mise publishes
binaries for amd64 and arm64 only. An all-s390x fleet never sees it, a
mixed fleet sees it along with the architectures that will fall back to
pyenv -- said before deploying, not discovered in a log an hour later.
mise goes into /usr/local/bin rather than ~/.local/bin: the remote command
runs through "ssh host 'command'", where neither ~/.profile nor ~/.bashrc
is read. Choosing "mise" exports "auto", not "mise", so a failed install
can still fall back to pyenv.
The unavailability notice was translated as "pyenv required", which said
the opposite of what happens: pyenv is the fallback, not a demand.
--- FR ---
Le fournisseur d'interpréteur se choisit désormais à la création d'une VM,
dans les deux interfaces. « mise » l'installe dans la VM et pose un
CPython précompilé ; « pyenv » garde la compilation d'aujourd'hui.
La question n'est posée que là où elle a un sens : mise ne publie de
binaires que pour amd64 et arm64. Un parc tout s390x ne la voit jamais, un
parc mixte la voit avec les architectures qui retomberont sur pyenv — dit
avant de déployer, pas découvert dans un log une heure plus tard.
mise va dans /usr/local/bin plutôt que ~/.local/bin : la commande distante
passe par « ssh hôte 'commande' », où ni ~/.profile ni ~/.bashrc ne sont
lus. Choisir « mise » exporte « auto », pas « mise », pour qu'une
installation ratée puisse encore retomber sur pyenv.
L'avis d'indisponibilité était traduit par « pyenv exigé », qui disait
l'inverse de ce qui se passe : pyenv est le repli, pas une exigence.
Assisted-by: Claude Opus 5
2026-08-11 22:11:23 -04:00
|
|
|
|
return ""
|
[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
|
|
|
|
priv = pub_path[:-4] if pub_path.endswith(".pub") else pub_path
|
|
|
|
|
|
return priv if os.path.exists(os.path.expanduser(priv)) else ""
|
[IMP] qemu: a VM one can reach, and a first boot that does not stall
The monitor froze the address given at launch, so it lost the VM as soon as
cloud-init renamed the host and DHCP handed out another lease. It now
re-resolves at each attempt, in the views too, and reads virsh without sudo
— « sudo -n » fails in a detached session with no tty.
First boot also stopped paying for what it does not need: the guest agent
leaves cloud-init, snapd and locale-gen go, apt takes the fastest mirror.
The timezone follows the host. And when KVM is missing, the deployment says
so before the wait instead of being mysteriously fifteen times slower.
--- FR ---
Le suivi figeait l'adresse connue au lancement : il perdait donc la VM dès
que cloud-init posait le vrai nom d'hôte et que DHCP donnait un autre bail.
Il la ré-résout désormais à chaque tentative, dans les vues aussi, et lit
virsh sans sudo — « sudo -n » échoue dans une session détachée, sans tty.
Le premier démarrage cesse aussi de payer l'inutile : l'agent invité sort
de cloud-init, snapd et locale-gen disparaissent, apt prend le miroir le
plus rapide. Le fuseau suit l'hôte. Et faute de KVM, le déploiement le dit
avant l'attente, au lieu d'être quinze fois plus lent sans raison visible.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -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
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _ssh_config_drop_hosts(content, names):
|
[FIX] LongTest : ne pas détruire sous une descente vivante ; blocs ssh
Relecture adversaire du commit précédent : il avait CRÉÉ un danger. Le rapport
s'écrivant maintenant VM par VM, celui de la descente EN COURS est le plus
récent, et « --detruire » l'aurait choisi — qm destroy --purge sur l'arbre que
le processus installait encore. Deux garde-fous : un PID dans le rapport, et
un refus net tant qu'un autre deep_proxmox.py tourne. Le second est nécessaire
car une descente déjà lancée a l'ancien module en mémoire.
Reconnu par ARGUMENT, pas par sous-chaîne : mon propre « pgrep -f
deep_proxmox.py » de surveillance donnait deux faux positifs sur trois.
Trois autres, mêmes preuves :
- un rapport vide plus récent masquait celui qui nommait les VM réelles ;
- detruire() ne créditait jamais l'étage 1 : le décompte était décalé de un
dans tous les cas, donc l'avertissement sortait toujours ;
- _ssh_config_drop_hosts prenait l'indentation pour de la syntaxe. Sur un bloc
au corps non indenté, seule la ligne Host partait et ssh rattachait
« StrictHostKeyChecking no » au bloc précédent — un serveur de production.
Et un bloc partagé (« Host prod-db vm-a ») partait en entier.
Les cinq correctifs meurent sous mutation.
--- EN ---
Adversarial review of the previous commit: it had CREATED a hazard. With the
report now written VM by VM, the RUNNING descent's is the most recent, and
"--detruire" would have picked it — qm destroy --purge on the tree the process
was still installing. Two guards: a PID in the report, and a flat refusal
while another deep_proxmox.py runs. The second is needed because an
already-running descent holds the old module in memory.
Matched by ARGUMENT, not substring: my own monitoring "pgrep -f
deep_proxmox.py" produced two false positives out of three.
Three more, same evidence:
- a newer empty report masked the one naming the real VMs;
- detruire() never credited level 1: the count was off by one in every case,
so the warning always fired;
- _ssh_config_drop_hosts took indentation for syntax. On a block with an
unindented body only the Host line went, and ssh attached
"StrictHostKeyChecking no" to the preceding block — a production server.
And a shared block ("Host prod-db vm-a") went entirely.
All five fixes die under mutation.
Assisted-by: claude-opus-5
(cherry picked from commit 7d348d976f96e420b4fec4d879911b658a730ffa)
2026-08-27 06:56:35 -04:00
|
|
|
|
"""Retire de ~/.ssh/config ce qui déclare l'un de `names`.
|
[IMP] qemu: a VM one can reach, and a first boot that does not stall
The monitor froze the address given at launch, so it lost the VM as soon as
cloud-init renamed the host and DHCP handed out another lease. It now
re-resolves at each attempt, in the views too, and reads virsh without sudo
— « sudo -n » fails in a detached session with no tty.
First boot also stopped paying for what it does not need: the guest agent
leaves cloud-init, snapd and locale-gen go, apt takes the fastest mirror.
The timezone follows the host. And when KVM is missing, the deployment says
so before the wait instead of being mysteriously fifteen times slower.
--- FR ---
Le suivi figeait l'adresse connue au lancement : il perdait donc la VM dès
que cloud-init posait le vrai nom d'hôte et que DHCP donnait un autre bail.
Il la ré-résout désormais à chaque tentative, dans les vues aussi, et lit
virsh sans sudo — « sudo -n » échoue dans une session détachée, sans tty.
Le premier démarrage cesse aussi de payer l'inutile : l'agent invité sort
de cloud-init, snapd et locale-gen disparaissent, apt prend le miroir le
plus rapide. Le fuseau suit l'hôte. Et faute de KVM, le déploiement le dit
avant l'attente, au lieu d'être quinze fois plus lent sans raison visible.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -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
|
|
|
|
On découpe en blocs plutôt que de substituer par expression
|
[FIX] LongTest : ne pas détruire sous une descente vivante ; blocs ssh
Relecture adversaire du commit précédent : il avait CRÉÉ un danger. Le rapport
s'écrivant maintenant VM par VM, celui de la descente EN COURS est le plus
récent, et « --detruire » l'aurait choisi — qm destroy --purge sur l'arbre que
le processus installait encore. Deux garde-fous : un PID dans le rapport, et
un refus net tant qu'un autre deep_proxmox.py tourne. Le second est nécessaire
car une descente déjà lancée a l'ancien module en mémoire.
Reconnu par ARGUMENT, pas par sous-chaîne : mon propre « pgrep -f
deep_proxmox.py » de surveillance donnait deux faux positifs sur trois.
Trois autres, mêmes preuves :
- un rapport vide plus récent masquait celui qui nommait les VM réelles ;
- detruire() ne créditait jamais l'étage 1 : le décompte était décalé de un
dans tous les cas, donc l'avertissement sortait toujours ;
- _ssh_config_drop_hosts prenait l'indentation pour de la syntaxe. Sur un bloc
au corps non indenté, seule la ligne Host partait et ssh rattachait
« StrictHostKeyChecking no » au bloc précédent — un serveur de production.
Et un bloc partagé (« Host prod-db vm-a ») partait en entier.
Les cinq correctifs meurent sous mutation.
--- EN ---
Adversarial review of the previous commit: it had CREATED a hazard. With the
report now written VM by VM, the RUNNING descent's is the most recent, and
"--detruire" would have picked it — qm destroy --purge on the tree the process
was still installing. Two guards: a PID in the report, and a flat refusal
while another deep_proxmox.py runs. The second is needed because an
already-running descent holds the old module in memory.
Matched by ARGUMENT, not substring: my own monitoring "pgrep -f
deep_proxmox.py" produced two false positives out of three.
Three more, same evidence:
- a newer empty report masked the one naming the real VMs;
- detruire() never credited level 1: the count was off by one in every case,
so the warning always fired;
- _ssh_config_drop_hosts took indentation for syntax. On a block with an
unindented body only the Host line went, and ssh attached
"StrictHostKeyChecking no" to the preceding block — a production server.
And a shared block ("Host prod-db vm-a") went entirely.
All five fixes die under mutation.
Assisted-by: claude-opus-5
(cherry picked from commit 7d348d976f96e420b4fec4d879911b658a730ffa)
2026-08-27 06:56:35 -04:00
|
|
|
|
régulière : une ligne Host peut porter PLUSIEURS noms.
|
|
|
|
|
|
|
[ADD] assistant : serveur LLM reconnu, catalogue gpt, sessions locales
L'entrée « Question IA » envoyait toute question à une seule API distante, sur
un modèle figé et sans historique : aucun serveur local n'était atteignable,
et rien ne disait où partait le texte.
Deux règles gouvernent ce qui la remplace. Un port ne dit jamais qui répond —
un seul en héberge jusqu'à trois, et une famille réémet l'API d'une autre en
entier — donc l'identité se lit dans le CORPS, par une échelle ordonnée. Et
une adresse ne devient jamais du texte d'invite : le détecteur du dépôt ne
voit pas les noms d'hôte, d'où une poignée opaque à sa place.
Vérifié : 284 tests, et contre un serveur du réseau — reconnaissance,
capacités lues, conversation multi-tours, triage juste.
--- EN ---
The "AI question" entry sent every question to a single remote API, on a
frozen model and with no history: no local server was reachable, and nothing
said where the text was going.
Two rules govern what replaces it. A port never says who answers — one hosts
up to three products, and one family re-serves another's native API in full —
so identity is read from the BODY, by an ordered ladder. And an address never
becomes prompt text: the repository's detector does not see host names, hence
an opaque handle in its place.
Checked: 284 tests, and against a server on the network — recognition,
capabilities read, a multi-turn conversation, a correct triage.
Assisted-by: Claude Opus 5
2026-09-09 06:41:56 -04:00
|
|
|
|
Deux règles, chacune corrigeant une perte de données que ce
|
|
|
|
|
|
découpage provoque sans elles.
|
[FIX] LongTest : ne pas détruire sous une descente vivante ; blocs ssh
Relecture adversaire du commit précédent : il avait CRÉÉ un danger. Le rapport
s'écrivant maintenant VM par VM, celui de la descente EN COURS est le plus
récent, et « --detruire » l'aurait choisi — qm destroy --purge sur l'arbre que
le processus installait encore. Deux garde-fous : un PID dans le rapport, et
un refus net tant qu'un autre deep_proxmox.py tourne. Le second est nécessaire
car une descente déjà lancée a l'ancien module en mémoire.
Reconnu par ARGUMENT, pas par sous-chaîne : mon propre « pgrep -f
deep_proxmox.py » de surveillance donnait deux faux positifs sur trois.
Trois autres, mêmes preuves :
- un rapport vide plus récent masquait celui qui nommait les VM réelles ;
- detruire() ne créditait jamais l'étage 1 : le décompte était décalé de un
dans tous les cas, donc l'avertissement sortait toujours ;
- _ssh_config_drop_hosts prenait l'indentation pour de la syntaxe. Sur un bloc
au corps non indenté, seule la ligne Host partait et ssh rattachait
« StrictHostKeyChecking no » au bloc précédent — un serveur de production.
Et un bloc partagé (« Host prod-db vm-a ») partait en entier.
Les cinq correctifs meurent sous mutation.
--- EN ---
Adversarial review of the previous commit: it had CREATED a hazard. With the
report now written VM by VM, the RUNNING descent's is the most recent, and
"--detruire" would have picked it — qm destroy --purge on the tree the process
was still installing. Two guards: a PID in the report, and a flat refusal
while another deep_proxmox.py runs. The second is needed because an
already-running descent holds the old module in memory.
Matched by ARGUMENT, not substring: my own monitoring "pgrep -f
deep_proxmox.py" produced two false positives out of three.
Three more, same evidence:
- a newer empty report masked the one naming the real VMs;
- detruire() never credited level 1: the count was off by one in every case,
so the warning always fired;
- _ssh_config_drop_hosts took indentation for syntax. On a block with an
unindented body only the Host line went, and ssh attached
"StrictHostKeyChecking no" to the preceding block — a production server.
And a shared block ("Host prod-db vm-a") went entirely.
All five fixes die under mutation.
Assisted-by: claude-opus-5
(cherry picked from commit 7d348d976f96e420b4fec4d879911b658a730ffa)
2026-08-27 06:56:35 -04:00
|
|
|
|
|
|
|
|
|
|
1. Seuls « Host » et « Match » clôturent un bloc. La règle d'avant —
|
|
|
|
|
|
« une ligne non indentée clôt le bloc » — prenait l'indentation
|
|
|
|
|
|
pour de la syntaxe, alors qu'elle est cosmétique dans ce format et
|
|
|
|
|
|
qu'un fichier écrit à la main s'en passe souvent. Sur un bloc au
|
|
|
|
|
|
corps non indenté, seule la ligne « Host » partait : HostName,
|
|
|
|
|
|
User, IdentityFile et « StrictHostKeyChecking no » restaient, sans
|
|
|
|
|
|
Host au-dessus, et ssh les rattachait au bloc PRÉCÉDENT. La
|
|
|
|
|
|
vérification de clé d'hôte se retrouvait désactivée sur un serveur
|
|
|
|
|
|
de production.
|
|
|
|
|
|
|
|
|
|
|
|
2. Un bloc qui déclare AUSSI des noms qu'on ne retire pas survit,
|
|
|
|
|
|
amputé de ceux-là seulement. Il partait en entier : « Host prod-db
|
|
|
|
|
|
vm-a » perdait le prod-db de l'utilisateur, et le surnom qu'on
|
|
|
|
|
|
ajoute à un bloc généré disparaissait au déploiement suivant.
|
|
|
|
|
|
|
|
|
|
|
|
La queue du bloc — lignes vides et commentaires — n'est pas emportée :
|
|
|
|
|
|
elle précède le plus souvent le bloc SUIVANT, et l'utilisateur y met
|
|
|
|
|
|
ses propres notes.
|
|
|
|
|
|
"""
|
[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
|
|
|
|
drop = set(names)
|
[FIX] LongTest : ne pas détruire sous une descente vivante ; blocs ssh
Relecture adversaire du commit précédent : il avait CRÉÉ un danger. Le rapport
s'écrivant maintenant VM par VM, celui de la descente EN COURS est le plus
récent, et « --detruire » l'aurait choisi — qm destroy --purge sur l'arbre que
le processus installait encore. Deux garde-fous : un PID dans le rapport, et
un refus net tant qu'un autre deep_proxmox.py tourne. Le second est nécessaire
car une descente déjà lancée a l'ancien module en mémoire.
Reconnu par ARGUMENT, pas par sous-chaîne : mon propre « pgrep -f
deep_proxmox.py » de surveillance donnait deux faux positifs sur trois.
Trois autres, mêmes preuves :
- un rapport vide plus récent masquait celui qui nommait les VM réelles ;
- detruire() ne créditait jamais l'étage 1 : le décompte était décalé de un
dans tous les cas, donc l'avertissement sortait toujours ;
- _ssh_config_drop_hosts prenait l'indentation pour de la syntaxe. Sur un bloc
au corps non indenté, seule la ligne Host partait et ssh rattachait
« StrictHostKeyChecking no » au bloc précédent — un serveur de production.
Et un bloc partagé (« Host prod-db vm-a ») partait en entier.
Les cinq correctifs meurent sous mutation.
--- EN ---
Adversarial review of the previous commit: it had CREATED a hazard. With the
report now written VM by VM, the RUNNING descent's is the most recent, and
"--detruire" would have picked it — qm destroy --purge on the tree the process
was still installing. Two guards: a PID in the report, and a flat refusal
while another deep_proxmox.py runs. The second is needed because an
already-running descent holds the old module in memory.
Matched by ARGUMENT, not substring: my own monitoring "pgrep -f
deep_proxmox.py" produced two false positives out of three.
Three more, same evidence:
- a newer empty report masked the one naming the real VMs;
- detruire() never credited level 1: the count was off by one in every case,
so the warning always fired;
- _ssh_config_drop_hosts took indentation for syntax. On a block with an
unindented body only the Host line went, and ssh attached
"StrictHostKeyChecking no" to the preceding block — a production server.
And a shared block ("Host prod-db vm-a") went entirely.
All five fixes die under mutation.
Assisted-by: claude-opus-5
(cherry picked from commit 7d348d976f96e420b4fec4d879911b658a730ffa)
2026-08-27 06:56:35 -04:00
|
|
|
|
out, block, block_names = [], [], []
|
[IMP] qemu: a VM one can reach, and a first boot that does not stall
The monitor froze the address given at launch, so it lost the VM as soon as
cloud-init renamed the host and DHCP handed out another lease. It now
re-resolves at each attempt, in the views too, and reads virsh without sudo
— « sudo -n » fails in a detached session with no tty.
First boot also stopped paying for what it does not need: the guest agent
leaves cloud-init, snapd and locale-gen go, apt takes the fastest mirror.
The timezone follows the host. And when KVM is missing, the deployment says
so before the wait instead of being mysteriously fifteen times slower.
--- FR ---
Le suivi figeait l'adresse connue au lancement : il perdait donc la VM dès
que cloud-init posait le vrai nom d'hôte et que DHCP donnait un autre bail.
Il la ré-résout désormais à chaque tentative, dans les vues aussi, et lit
virsh sans sudo — « sudo -n » échoue dans une session détachée, sans tty.
Le premier démarrage cesse aussi de payer l'inutile : l'agent invité sort
de cloud-init, snapd et locale-gen disparaissent, apt prend le miroir le
plus rapide. Le fuseau suit l'hôte. Et faute de KVM, le déploiement le dit
avant l'attente, au lieu d'être quinze fois plus lent sans raison visible.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -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
|
|
|
|
def flush():
|
[FIX] LongTest : ne pas détruire sous une descente vivante ; blocs ssh
Relecture adversaire du commit précédent : il avait CRÉÉ un danger. Le rapport
s'écrivant maintenant VM par VM, celui de la descente EN COURS est le plus
récent, et « --detruire » l'aurait choisi — qm destroy --purge sur l'arbre que
le processus installait encore. Deux garde-fous : un PID dans le rapport, et
un refus net tant qu'un autre deep_proxmox.py tourne. Le second est nécessaire
car une descente déjà lancée a l'ancien module en mémoire.
Reconnu par ARGUMENT, pas par sous-chaîne : mon propre « pgrep -f
deep_proxmox.py » de surveillance donnait deux faux positifs sur trois.
Trois autres, mêmes preuves :
- un rapport vide plus récent masquait celui qui nommait les VM réelles ;
- detruire() ne créditait jamais l'étage 1 : le décompte était décalé de un
dans tous les cas, donc l'avertissement sortait toujours ;
- _ssh_config_drop_hosts prenait l'indentation pour de la syntaxe. Sur un bloc
au corps non indenté, seule la ligne Host partait et ssh rattachait
« StrictHostKeyChecking no » au bloc précédent — un serveur de production.
Et un bloc partagé (« Host prod-db vm-a ») partait en entier.
Les cinq correctifs meurent sous mutation.
--- EN ---
Adversarial review of the previous commit: it had CREATED a hazard. With the
report now written VM by VM, the RUNNING descent's is the most recent, and
"--detruire" would have picked it — qm destroy --purge on the tree the process
was still installing. Two guards: a PID in the report, and a flat refusal
while another deep_proxmox.py runs. The second is needed because an
already-running descent holds the old module in memory.
Matched by ARGUMENT, not substring: my own monitoring "pgrep -f
deep_proxmox.py" produced two false positives out of three.
Three more, same evidence:
- a newer empty report masked the one naming the real VMs;
- detruire() never credited level 1: the count was off by one in every case,
so the warning always fired;
- _ssh_config_drop_hosts took indentation for syntax. On a block with an
unindented body only the Host line went, and ssh attached
"StrictHostKeyChecking no" to the preceding block — a production server.
And a shared block ("Host prod-db vm-a") went entirely.
All five fixes die under mutation.
Assisted-by: claude-opus-5
(cherry picked from commit 7d348d976f96e420b4fec4d879911b658a730ffa)
2026-08-27 06:56:35 -04:00
|
|
|
|
if not block:
|
|
|
|
|
|
return
|
|
|
|
|
|
restants = [n for n in block_names if n not in drop]
|
|
|
|
|
|
if restants == block_names:
|
[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
|
|
|
|
out.extend(block)
|
[FIX] LongTest : ne pas détruire sous une descente vivante ; blocs ssh
Relecture adversaire du commit précédent : il avait CRÉÉ un danger. Le rapport
s'écrivant maintenant VM par VM, celui de la descente EN COURS est le plus
récent, et « --detruire » l'aurait choisi — qm destroy --purge sur l'arbre que
le processus installait encore. Deux garde-fous : un PID dans le rapport, et
un refus net tant qu'un autre deep_proxmox.py tourne. Le second est nécessaire
car une descente déjà lancée a l'ancien module en mémoire.
Reconnu par ARGUMENT, pas par sous-chaîne : mon propre « pgrep -f
deep_proxmox.py » de surveillance donnait deux faux positifs sur trois.
Trois autres, mêmes preuves :
- un rapport vide plus récent masquait celui qui nommait les VM réelles ;
- detruire() ne créditait jamais l'étage 1 : le décompte était décalé de un
dans tous les cas, donc l'avertissement sortait toujours ;
- _ssh_config_drop_hosts prenait l'indentation pour de la syntaxe. Sur un bloc
au corps non indenté, seule la ligne Host partait et ssh rattachait
« StrictHostKeyChecking no » au bloc précédent — un serveur de production.
Et un bloc partagé (« Host prod-db vm-a ») partait en entier.
Les cinq correctifs meurent sous mutation.
--- EN ---
Adversarial review of the previous commit: it had CREATED a hazard. With the
report now written VM by VM, the RUNNING descent's is the most recent, and
"--detruire" would have picked it — qm destroy --purge on the tree the process
was still installing. Two guards: a PID in the report, and a flat refusal
while another deep_proxmox.py runs. The second is needed because an
already-running descent holds the old module in memory.
Matched by ARGUMENT, not substring: my own monitoring "pgrep -f
deep_proxmox.py" produced two false positives out of three.
Three more, same evidence:
- a newer empty report masked the one naming the real VMs;
- detruire() never credited level 1: the count was off by one in every case,
so the warning always fired;
- _ssh_config_drop_hosts took indentation for syntax. On a block with an
unindented body only the Host line went, and ssh attached
"StrictHostKeyChecking no" to the preceding block — a production server.
And a shared block ("Host prod-db vm-a") went entirely.
All five fixes die under mutation.
Assisted-by: claude-opus-5
(cherry picked from commit 7d348d976f96e420b4fec4d879911b658a730ffa)
2026-08-27 06:56:35 -04:00
|
|
|
|
return
|
|
|
|
|
|
fin = len(block)
|
|
|
|
|
|
while fin > 1 and (
|
|
|
|
|
|
not block[fin - 1].strip()
|
|
|
|
|
|
or block[fin - 1].lstrip().startswith("#")
|
|
|
|
|
|
):
|
|
|
|
|
|
fin -= 1
|
|
|
|
|
|
if restants:
|
|
|
|
|
|
tete = block[0]
|
|
|
|
|
|
marge = tete[: len(tete) - len(tete.lstrip())]
|
|
|
|
|
|
out.append(f"{marge}Host {' '.join(restants)}\n")
|
|
|
|
|
|
out.extend(block[1:fin])
|
|
|
|
|
|
out.extend(block[fin:])
|
[IMP] qemu: a VM one can reach, and a first boot that does not stall
The monitor froze the address given at launch, so it lost the VM as soon as
cloud-init renamed the host and DHCP handed out another lease. It now
re-resolves at each attempt, in the views too, and reads virsh without sudo
— « sudo -n » fails in a detached session with no tty.
First boot also stopped paying for what it does not need: the guest agent
leaves cloud-init, snapd and locale-gen go, apt takes the fastest mirror.
The timezone follows the host. And when KVM is missing, the deployment says
so before the wait instead of being mysteriously fifteen times slower.
--- FR ---
Le suivi figeait l'adresse connue au lancement : il perdait donc la VM dès
que cloud-init posait le vrai nom d'hôte et que DHCP donnait un autre bail.
Il la ré-résout désormais à chaque tentative, dans les vues aussi, et lit
virsh sans sudo — « sudo -n » échoue dans une session détachée, sans tty.
Le premier démarrage cesse aussi de payer l'inutile : l'agent invité sort
de cloud-init, snapd et locale-gen disparaissent, apt prend le miroir le
plus rapide. Le fuseau suit l'hôte. Et faute de KVM, le déploiement le dit
avant l'attente, au lieu d'être quinze fois plus lent sans raison visible.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -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
|
|
|
|
for line in content.splitlines(keepends=True):
|
[FIX] LongTest : ne pas détruire sous une descente vivante ; blocs ssh
Relecture adversaire du commit précédent : il avait CRÉÉ un danger. Le rapport
s'écrivant maintenant VM par VM, celui de la descente EN COURS est le plus
récent, et « --detruire » l'aurait choisi — qm destroy --purge sur l'arbre que
le processus installait encore. Deux garde-fous : un PID dans le rapport, et
un refus net tant qu'un autre deep_proxmox.py tourne. Le second est nécessaire
car une descente déjà lancée a l'ancien module en mémoire.
Reconnu par ARGUMENT, pas par sous-chaîne : mon propre « pgrep -f
deep_proxmox.py » de surveillance donnait deux faux positifs sur trois.
Trois autres, mêmes preuves :
- un rapport vide plus récent masquait celui qui nommait les VM réelles ;
- detruire() ne créditait jamais l'étage 1 : le décompte était décalé de un
dans tous les cas, donc l'avertissement sortait toujours ;
- _ssh_config_drop_hosts prenait l'indentation pour de la syntaxe. Sur un bloc
au corps non indenté, seule la ligne Host partait et ssh rattachait
« StrictHostKeyChecking no » au bloc précédent — un serveur de production.
Et un bloc partagé (« Host prod-db vm-a ») partait en entier.
Les cinq correctifs meurent sous mutation.
--- EN ---
Adversarial review of the previous commit: it had CREATED a hazard. With the
report now written VM by VM, the RUNNING descent's is the most recent, and
"--detruire" would have picked it — qm destroy --purge on the tree the process
was still installing. Two guards: a PID in the report, and a flat refusal
while another deep_proxmox.py runs. The second is needed because an
already-running descent holds the old module in memory.
Matched by ARGUMENT, not substring: my own monitoring "pgrep -f
deep_proxmox.py" produced two false positives out of three.
Three more, same evidence:
- a newer empty report masked the one naming the real VMs;
- detruire() never credited level 1: the count was off by one in every case,
so the warning always fired;
- _ssh_config_drop_hosts took indentation for syntax. On a block with an
unindented body only the Host line went, and ssh attached
"StrictHostKeyChecking no" to the preceding block — a production server.
And a shared block ("Host prod-db vm-a") went entirely.
All five fixes die under mutation.
Assisted-by: claude-opus-5
(cherry picked from commit 7d348d976f96e420b4fec4d879911b658a730ffa)
2026-08-27 06:56:35 -04:00
|
|
|
|
if re.match(r"^[ \t]*Host[ \t]+", line, re.I):
|
[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
|
|
|
|
flush()
|
|
|
|
|
|
block = [line]
|
[FIX] LongTest : ne pas détruire sous une descente vivante ; blocs ssh
Relecture adversaire du commit précédent : il avait CRÉÉ un danger. Le rapport
s'écrivant maintenant VM par VM, celui de la descente EN COURS est le plus
récent, et « --detruire » l'aurait choisi — qm destroy --purge sur l'arbre que
le processus installait encore. Deux garde-fous : un PID dans le rapport, et
un refus net tant qu'un autre deep_proxmox.py tourne. Le second est nécessaire
car une descente déjà lancée a l'ancien module en mémoire.
Reconnu par ARGUMENT, pas par sous-chaîne : mon propre « pgrep -f
deep_proxmox.py » de surveillance donnait deux faux positifs sur trois.
Trois autres, mêmes preuves :
- un rapport vide plus récent masquait celui qui nommait les VM réelles ;
- detruire() ne créditait jamais l'étage 1 : le décompte était décalé de un
dans tous les cas, donc l'avertissement sortait toujours ;
- _ssh_config_drop_hosts prenait l'indentation pour de la syntaxe. Sur un bloc
au corps non indenté, seule la ligne Host partait et ssh rattachait
« StrictHostKeyChecking no » au bloc précédent — un serveur de production.
Et un bloc partagé (« Host prod-db vm-a ») partait en entier.
Les cinq correctifs meurent sous mutation.
--- EN ---
Adversarial review of the previous commit: it had CREATED a hazard. With the
report now written VM by VM, the RUNNING descent's is the most recent, and
"--detruire" would have picked it — qm destroy --purge on the tree the process
was still installing. Two guards: a PID in the report, and a flat refusal
while another deep_proxmox.py runs. The second is needed because an
already-running descent holds the old module in memory.
Matched by ARGUMENT, not substring: my own monitoring "pgrep -f
deep_proxmox.py" produced two false positives out of three.
Three more, same evidence:
- a newer empty report masked the one naming the real VMs;
- detruire() never credited level 1: the count was off by one in every case,
so the warning always fired;
- _ssh_config_drop_hosts took indentation for syntax. On a block with an
unindented body only the Host line went, and ssh attached
"StrictHostKeyChecking no" to the preceding block — a production server.
And a shared block ("Host prod-db vm-a") went entirely.
All five fixes die under mutation.
Assisted-by: claude-opus-5
(cherry picked from commit 7d348d976f96e420b4fec4d879911b658a730ffa)
2026-08-27 06:56:35 -04:00
|
|
|
|
block_names = line.split()[1:]
|
|
|
|
|
|
elif re.match(r"^[ \t]*Match[ \t]+", line, re.I):
|
|
|
|
|
|
# Match ouvre une section qui n'appartient à aucun Host : la
|
|
|
|
|
|
# garder telle quelle, quel que soit le sort du bloc d'avant.
|
|
|
|
|
|
flush()
|
|
|
|
|
|
block, block_names = [], []
|
|
|
|
|
|
out.append(line)
|
[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
|
|
|
|
elif block:
|
[FIX] LongTest : ne pas détruire sous une descente vivante ; blocs ssh
Relecture adversaire du commit précédent : il avait CRÉÉ un danger. Le rapport
s'écrivant maintenant VM par VM, celui de la descente EN COURS est le plus
récent, et « --detruire » l'aurait choisi — qm destroy --purge sur l'arbre que
le processus installait encore. Deux garde-fous : un PID dans le rapport, et
un refus net tant qu'un autre deep_proxmox.py tourne. Le second est nécessaire
car une descente déjà lancée a l'ancien module en mémoire.
Reconnu par ARGUMENT, pas par sous-chaîne : mon propre « pgrep -f
deep_proxmox.py » de surveillance donnait deux faux positifs sur trois.
Trois autres, mêmes preuves :
- un rapport vide plus récent masquait celui qui nommait les VM réelles ;
- detruire() ne créditait jamais l'étage 1 : le décompte était décalé de un
dans tous les cas, donc l'avertissement sortait toujours ;
- _ssh_config_drop_hosts prenait l'indentation pour de la syntaxe. Sur un bloc
au corps non indenté, seule la ligne Host partait et ssh rattachait
« StrictHostKeyChecking no » au bloc précédent — un serveur de production.
Et un bloc partagé (« Host prod-db vm-a ») partait en entier.
Les cinq correctifs meurent sous mutation.
--- EN ---
Adversarial review of the previous commit: it had CREATED a hazard. With the
report now written VM by VM, the RUNNING descent's is the most recent, and
"--detruire" would have picked it — qm destroy --purge on the tree the process
was still installing. Two guards: a PID in the report, and a flat refusal
while another deep_proxmox.py runs. The second is needed because an
already-running descent holds the old module in memory.
Matched by ARGUMENT, not substring: my own monitoring "pgrep -f
deep_proxmox.py" produced two false positives out of three.
Three more, same evidence:
- a newer empty report masked the one naming the real VMs;
- detruire() never credited level 1: the count was off by one in every case,
so the warning always fired;
- _ssh_config_drop_hosts took indentation for syntax. On a block with an
unindented body only the Host line went, and ssh attached
"StrictHostKeyChecking no" to the preceding block — a production server.
And a shared block ("Host prod-db vm-a") went entirely.
All five fixes die under mutation.
Assisted-by: claude-opus-5
(cherry picked from commit 7d348d976f96e420b4fec4d879911b658a730ffa)
2026-08-27 06:56:35 -04:00
|
|
|
|
block.append(line)
|
[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
|
|
|
|
else:
|
|
|
|
|
|
out.append(line)
|
|
|
|
|
|
flush()
|
|
|
|
|
|
return "".join(out)
|
[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
|
|
|
|
|
[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
|
|
|
|
def _write_ssh_config_entry(
|
[FIX] proxmox : l'ancienne entrée ssh s'en va avec la convention
Le nom chaîné devient systématique, mais les entrées écrites AVANT portent le
nom court — et rien ne les retirerait : elles ne déclarent pas le nom qu'on
écrit maintenant. Deux blocs mèneraient à la même machine, exactement ce
qu'on venait d'enlever.
Le ProxyJump tranche : un bloc qui rebondit par CET hôte est le nôtre, on le
retire. Celui d'une VM locale homonyme n'en a pas, et on n'y touche jamais ;
celui d'un autre hôte Proxmox non plus.
Le drapeau Odoo gagne son test au passage. Il tombait pour la même raison que
les colonnes vides — la sonde est le dernier maillon de la suite distante, et
un parc où une seule VM n'a pas d'Odoo, un hyperviseur imbriqué par exemple,
finit en échec. Vérifié sur les trois VM : l'hôte rend bien « ODOO » pour les
deux qui écoutent, et le navigateur répondait 303 pendant que la colonne
disait « — ».
--- EN ---
The chained name becomes systematic, but entries written BEFORE carry the
short one — and nothing would retire them: they do not declare the name we
now write. Two blocks would lead to the same machine, exactly what we had
just removed.
The ProxyJump decides: a block hopping through THIS host is ours, so it goes.
A local namesake's has none, and is never touched; another Proxmox host's
neither.
The Odoo flag gains its test along the way. It failed for the same reason as
the empty columns — the probe is the remote pipeline's last link, and a fleet
where a single VM has no Odoo, a nested hypervisor for instance, ends in
failure. Verified on all three VMs: the host does return "ODOO" for the two
that listen, and the browser answered 303 while the column said "—".
Assisted-by: Claude Opus 5
2026-08-25 00:43:10 -04:00
|
|
|
|
self,
|
|
|
|
|
|
host,
|
|
|
|
|
|
user,
|
|
|
|
|
|
ip,
|
|
|
|
|
|
proxy_jump=None,
|
|
|
|
|
|
identity_file=None,
|
|
|
|
|
|
also_drop=(),
|
[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
|
|
|
|
):
|
|
|
|
|
|
"""Écrit/remplace un bloc « Host <host> » dans ~/.ssh/config.
|
[IMP] qemu: a VM one can reach, and a first boot that does not stall
The monitor froze the address given at launch, so it lost the VM as soon as
cloud-init renamed the host and DHCP handed out another lease. It now
re-resolves at each attempt, in the views too, and reads virsh without sudo
— « sudo -n » fails in a detached session with no tty.
First boot also stopped paying for what it does not need: the guest agent
leaves cloud-init, snapd and locale-gen go, apt takes the fastest mirror.
The timezone follows the host. And when KVM is missing, the deployment says
so before the wait instead of being mysteriously fifteen times slower.
--- FR ---
Le suivi figeait l'adresse connue au lancement : il perdait donc la VM dès
que cloud-init posait le vrai nom d'hôte et que DHCP donnait un autre bail.
Il la ré-résout désormais à chaque tentative, dans les vues aussi, et lit
virsh sans sudo — « sudo -n » échoue dans une session détachée, sans tty.
Le premier démarrage cesse aussi de payer l'inutile : l'agent invité sort
de cloud-init, snapd et locale-gen disparaissent, apt prend le miroir le
plus rapide. Le fuseau suit l'hôte. Et faute de KVM, le déploiement le dit
avant l'attente, au lieu d'être quinze fois plus lent sans raison visible.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -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
|
|
|
|
`host` peut être une liste de noms : ils partagent alors un seul bloc.
|
[FIX] proxmox : l'ancienne entrée ssh s'en va avec la convention
Le nom chaîné devient systématique, mais les entrées écrites AVANT portent le
nom court — et rien ne les retirerait : elles ne déclarent pas le nom qu'on
écrit maintenant. Deux blocs mèneraient à la même machine, exactement ce
qu'on venait d'enlever.
Le ProxyJump tranche : un bloc qui rebondit par CET hôte est le nôtre, on le
retire. Celui d'une VM locale homonyme n'en a pas, et on n'y touche jamais ;
celui d'un autre hôte Proxmox non plus.
Le drapeau Odoo gagne son test au passage. Il tombait pour la même raison que
les colonnes vides — la sonde est le dernier maillon de la suite distante, et
un parc où une seule VM n'a pas d'Odoo, un hyperviseur imbriqué par exemple,
finit en échec. Vérifié sur les trois VM : l'hôte rend bien « ODOO » pour les
deux qui écoutent, et le navigateur répondait 303 pendant que la colonne
disait « — ».
--- EN ---
The chained name becomes systematic, but entries written BEFORE carry the
short one — and nothing would retire them: they do not declare the name we
now write. Two blocks would lead to the same machine, exactly what we had
just removed.
The ProxyJump decides: a block hopping through THIS host is ours, so it goes.
A local namesake's has none, and is never touched; another Proxmox host's
neither.
The Odoo flag gains its test along the way. It failed for the same reason as
the empty columns — the probe is the remote pipeline's last link, and a fleet
where a single VM has no Odoo, a nested hypervisor for instance, ends in
failure. Verified on all three VMs: the host does return "ODOO" for the two
that listen, and the browser answered 303 while the column said "—".
Assisted-by: Claude Opus 5
2026-08-25 00:43:10 -04:00
|
|
|
|
|
|
|
|
|
|
`also_drop` : noms dont le bloc doit DISPARAÎTRE sans être réécrit.
|
|
|
|
|
|
Sert quand une convention de nommage change : l'ancienne entrée ne
|
|
|
|
|
|
désigne pas le nom qu'on écrit, donc rien ne la retirerait, et deux
|
|
|
|
|
|
blocs finiraient par mener à la même machine — ce qu'on venait
|
|
|
|
|
|
justement d'enlever. L'appelant vérifie que l'ancien bloc est BIEN le
|
|
|
|
|
|
sien avant de le nommer ici.
|
[IMP] qemu: a VM one can reach, and a first boot that does not stall
The monitor froze the address given at launch, so it lost the VM as soon as
cloud-init renamed the host and DHCP handed out another lease. It now
re-resolves at each attempt, in the views too, and reads virsh without sudo
— « sudo -n » fails in a detached session with no tty.
First boot also stopped paying for what it does not need: the guest agent
leaves cloud-init, snapd and locale-gen go, apt takes the fastest mirror.
The timezone follows the host. And when KVM is missing, the deployment says
so before the wait instead of being mysteriously fifteen times slower.
--- FR ---
Le suivi figeait l'adresse connue au lancement : il perdait donc la VM dès
que cloud-init posait le vrai nom d'hôte et que DHCP donnait un autre bail.
Il la ré-résout désormais à chaque tentative, dans les vues aussi, et lit
virsh sans sudo — « sudo -n » échoue dans une session détachée, sans tty.
Le premier démarrage cesse aussi de payer l'inutile : l'agent invité sort
de cloud-init, snapd et locale-gen disparaissent, apt prend le miroir le
plus rapide. Le fuseau suit l'hôte. Et faute de KVM, le déploiement le dit
avant l'attente, au lieu d'être quinze fois plus lent sans raison visible.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -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
|
|
|
|
`proxy_jump` : alias du rebond pour une VM imbriquée, dont l'IP n'est
|
|
|
|
|
|
joignable que depuis son hôte. OpenSSH enchaîne les ProxyJump tout
|
|
|
|
|
|
seul dès que le parent a lui-même le sien.
|
[IMP] qemu: a VM one can reach, and a first boot that does not stall
The monitor froze the address given at launch, so it lost the VM as soon as
cloud-init renamed the host and DHCP handed out another lease. It now
re-resolves at each attempt, in the views too, and reads virsh without sudo
— « sudo -n » fails in a detached session with no tty.
First boot also stopped paying for what it does not need: the guest agent
leaves cloud-init, snapd and locale-gen go, apt takes the fastest mirror.
The timezone follows the host. And when KVM is missing, the deployment says
so before the wait instead of being mysteriously fifteen times slower.
--- FR ---
Le suivi figeait l'adresse connue au lancement : il perdait donc la VM dès
que cloud-init posait le vrai nom d'hôte et que DHCP donnait un autre bail.
Il la ré-résout désormais à chaque tentative, dans les vues aussi, et lit
virsh sans sudo — « sudo -n » échoue dans une session détachée, sans tty.
Le premier démarrage cesse aussi de payer l'inutile : l'agent invité sort
de cloud-init, snapd et locale-gen disparaissent, apt prend le miroir le
plus rapide. Le fuseau suit l'hôte. Et faute de KVM, le déploiement le dit
avant l'attente, au lieu d'être quinze fois plus lent sans raison visible.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -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
|
|
|
|
`identity_file` : clé PRIVÉE à présenter. Sans elle, ssh propose
|
|
|
|
|
|
toutes les identités de l'agent et un parc un peu fourni déclenche
|
|
|
|
|
|
« Too many authentication failures » avant d'arriver à la bonne."""
|
|
|
|
|
|
names = [host] if isinstance(host, str) else list(host)
|
|
|
|
|
|
cfg = os.path.expanduser("~/.ssh/config")
|
|
|
|
|
|
os.makedirs(os.path.dirname(cfg), exist_ok=True)
|
|
|
|
|
|
existing = ""
|
|
|
|
|
|
if os.path.exists(cfg):
|
|
|
|
|
|
with open(cfg, encoding="utf-8") as fh:
|
|
|
|
|
|
existing = fh.read()
|
[FIX] proxmox : l'ancienne entrée ssh s'en va avec la convention
Le nom chaîné devient systématique, mais les entrées écrites AVANT portent le
nom court — et rien ne les retirerait : elles ne déclarent pas le nom qu'on
écrit maintenant. Deux blocs mèneraient à la même machine, exactement ce
qu'on venait d'enlever.
Le ProxyJump tranche : un bloc qui rebondit par CET hôte est le nôtre, on le
retire. Celui d'une VM locale homonyme n'en a pas, et on n'y touche jamais ;
celui d'un autre hôte Proxmox non plus.
Le drapeau Odoo gagne son test au passage. Il tombait pour la même raison que
les colonnes vides — la sonde est le dernier maillon de la suite distante, et
un parc où une seule VM n'a pas d'Odoo, un hyperviseur imbriqué par exemple,
finit en échec. Vérifié sur les trois VM : l'hôte rend bien « ODOO » pour les
deux qui écoutent, et le navigateur répondait 303 pendant que la colonne
disait « — ».
--- EN ---
The chained name becomes systematic, but entries written BEFORE carry the
short one — and nothing would retire them: they do not declare the name we
now write. Two blocks would lead to the same machine, exactly what we had
just removed.
The ProxyJump decides: a block hopping through THIS host is ours, so it goes.
A local namesake's has none, and is never touched; another Proxmox host's
neither.
The Odoo flag gains its test along the way. It failed for the same reason as
the empty columns — the probe is the remote pipeline's last link, and a fleet
where a single VM has no Odoo, a nested hypervisor for instance, ends in
failure. Verified on all three VMs: the host does return "ODOO" for the two
that listen, and the browser answered 303 while the column said "—".
Assisted-by: Claude Opus 5
2026-08-25 00:43:10 -04:00
|
|
|
|
existing = self._ssh_config_drop_hosts(
|
|
|
|
|
|
existing, names + [n for n in also_drop if n not in names]
|
|
|
|
|
|
).rstrip("\n")
|
[FIX] LongTest : rapport écrit VM par VM, retrait ssh sans bloc nu
Descente de dix étages arrêtée pendant l'installation du quatrième : quatre
machines réelles restaient, et « --detruire » répondait « aucun rapport : rien
à défaire ». Le rapport ne s'écrivait qu'à la fin, donc le seul enregistrement
du couple (alias du parent, VMID) mourait avec le processus. Il fallait les
retrouver par leur NOM, ce que tout ce fichier s'applique à éviter.
En nettoyant à la main, second défaut : appelé sans nom à écrire — un retrait
pur, légitime, les machines n'existant plus — _write_ssh_config_entry écrivait
« Host » NU suivi d'un « HostName » vide dans le ~/.ssh/config réel, puis
mourait sur IndexError en annonçant l'ajout. Constaté, puis retiré du fichier.
Les deux correctifs meurent sous mutation : 3 tests et 2 tests.
--- EN ---
A ten-level descent stopped during the fourth level's install: four real
machines were left, and "--detruire" answered "no report: nothing to undo".
The report was only written at the end, so the sole record of the (parent
alias, VMID) pair died with the process. They had to be found by NAME, which
this whole file works to avoid.
Cleaning up by hand surfaced a second defect: called with no name to write — a
pure removal, legitimate since the machines are gone — _write_ssh_config_entry
wrote a BARE "Host" followed by an empty "HostName" into the real ~/.ssh/config,
then died on IndexError while announcing the addition. Observed, then removed.
Both fixes die under mutation: 3 tests and 2 tests.
Assisted-by: claude-opus-5
(cherry picked from commit e7c8b540c630b33c6eb1b1a2f51c01497e0b3ca0)
2026-08-27 05:39:08 -04:00
|
|
|
|
if not names:
|
|
|
|
|
|
# Retirer sans réécrire est un appel légitime : les machines
|
|
|
|
|
|
# n'existent plus. Sans ce retour, un « Host » NU était écrit dans
|
|
|
|
|
|
# le ~/.ssh/config de l'utilisateur — un bloc sans nom, suivi d'un
|
|
|
|
|
|
# « HostName » vide, qui s'applique alors à rien et brouille la
|
|
|
|
|
|
# lecture du fichier.
|
|
|
|
|
|
with open(cfg, "w", encoding="utf-8") as fh:
|
|
|
|
|
|
fh.write(existing + "\n" if existing else "")
|
|
|
|
|
|
os.chmod(cfg, 0o600)
|
|
|
|
|
|
retires = ", ".join(also_drop)
|
|
|
|
|
|
print(f"🗑 {t('Removed from ~/.ssh/config:')} {retires}")
|
|
|
|
|
|
return
|
[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
|
|
|
|
block = (
|
|
|
|
|
|
f"Host {' '.join(names)}\n"
|
|
|
|
|
|
f" HostName {ip}\n"
|
|
|
|
|
|
f" User {user}\n"
|
|
|
|
|
|
# IP DHCP réutilisées entre VM -> on évite l'erreur de clé d'hôte.
|
|
|
|
|
|
f" StrictHostKeyChecking no\n"
|
|
|
|
|
|
f" UserKnownHostsFile /dev/null\n"
|
[IMP] qemu: a VM one can reach, and a first boot that does not stall
The monitor froze the address given at launch, so it lost the VM as soon as
cloud-init renamed the host and DHCP handed out another lease. It now
re-resolves at each attempt, in the views too, and reads virsh without sudo
— « sudo -n » fails in a detached session with no tty.
First boot also stopped paying for what it does not need: the guest agent
leaves cloud-init, snapd and locale-gen go, apt takes the fastest mirror.
The timezone follows the host. And when KVM is missing, the deployment says
so before the wait instead of being mysteriously fifteen times slower.
--- FR ---
Le suivi figeait l'adresse connue au lancement : il perdait donc la VM dès
que cloud-init posait le vrai nom d'hôte et que DHCP donnait un autre bail.
Il la ré-résout désormais à chaque tentative, dans les vues aussi, et lit
virsh sans sudo — « sudo -n » échoue dans une session détachée, sans tty.
Le premier démarrage cesse aussi de payer l'inutile : l'agent invité sort
de cloud-init, snapd et locale-gen disparaissent, apt prend le miroir le
plus rapide. Le fuseau suit l'hôte. Et faute de KVM, le déploiement le dit
avant l'attente, au lieu d'être quinze fois plus lent sans raison visible.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -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
|
|
|
|
if identity_file:
|
|
|
|
|
|
# IdentitiesOnly : sans lui, IdentityFile s'AJOUTE aux clés de
|
|
|
|
|
|
# l'agent au lieu de les remplacer, et le serveur coupe après
|
|
|
|
|
|
# 5 essais infructueux.
|
|
|
|
|
|
block += (
|
[ADD] déploiement : un proxy SOCKS par SSH, et son mode d'emploi
« -L » relaie UN service ; « -D » ouvre un relais SOCKS, par lequel le
navigateur atteint n'importe quelle destination depuis la machine distante —
une interface qui n'écoute que sur sa boucle locale, un hôte de son réseau sans
route depuis ici. Le port par défaut est 1080, modifiable, et contrôlé libre
avant d'ouvrir. La saisie de l'adresse est celle de sshfs, désormais partagée :
l'alias de ~/.ssh/config part tel quel à ssh, faute de quoi son ProxyJump se
perdrait et une VM imbriquée deviendrait injoignable. Le réglage de Firefox
s'affiche AVANT le lancement, la commande ne rendant la main qu'au Ctrl+C.
Vérifié : 9 tests neufs, et la numérotation du menu que deux autres gardent.
--- EN ---
"-L" relays ONE service; "-D" opens a SOCKS relay, through which the browser
reaches any destination from the remote machine — an interface listening only
on its loopback, a host of its network with no route from here. The default
port is 1080, changeable, and checked free before opening. The address prompt
is sshfs's, now shared: the ~/.ssh/config alias goes to ssh as is, failing
which its ProxyJump would be lost and a nested VM become unreachable. The
Firefox settings print BEFORE the launch, the command only returning on
Ctrl+C.
Checked: 9 new tests, and the menu numbering two others guard.
Assisted-by: Claude Opus 5
2026-09-24 13:32:28 -04:00
|
|
|
|
f" IdentityFile {identity_file}\n IdentitiesOnly yes\n"
|
2026-08-11 08:59:10 -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
|
|
|
|
if proxy_jump:
|
|
|
|
|
|
block += f" ProxyJump {proxy_jump}\n"
|
|
|
|
|
|
content = (existing + "\n\n" + block) if existing else block
|
|
|
|
|
|
with open(cfg, "w", encoding="utf-8") as fh:
|
|
|
|
|
|
fh.write(content)
|
|
|
|
|
|
os.chmod(cfg, 0o600)
|
|
|
|
|
|
print(f"✅ {t('Added to ~/.ssh/config:')} ssh {names[0]}")
|
[IMP] qemu: a VM one can reach, and a first boot that does not stall
The monitor froze the address given at launch, so it lost the VM as soon as
cloud-init renamed the host and DHCP handed out another lease. It now
re-resolves at each attempt, in the views too, and reads virsh without sudo
— « sudo -n » fails in a detached session with no tty.
First boot also stopped paying for what it does not need: the guest agent
leaves cloud-init, snapd and locale-gen go, apt takes the fastest mirror.
The timezone follows the host. And when KVM is missing, the deployment says
so before the wait instead of being mysteriously fifteen times slower.
--- FR ---
Le suivi figeait l'adresse connue au lancement : il perdait donc la VM dès
que cloud-init posait le vrai nom d'hôte et que DHCP donnait un autre bail.
Il la ré-résout désormais à chaque tentative, dans les vues aussi, et lit
virsh sans sudo — « sudo -n » échoue dans une session détachée, sans tty.
Le premier démarrage cesse aussi de payer l'inutile : l'agent invité sort
de cloud-init, snapd et locale-gen disparaissent, apt prend le miroir le
plus rapide. Le fuseau suit l'hôte. Et faute de KVM, le déploiement le dit
avant l'attente, au lieu d'être quinze fois plus lent sans raison visible.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -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
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _human_size(n):
|
|
|
|
|
|
"""Octets -> taille lisible (Ko/Mo/Go…)."""
|
|
|
|
|
|
size = float(n)
|
|
|
|
|
|
for unit in ("o", "Ko", "Mo", "Go", "To"):
|
|
|
|
|
|
if size < 1024:
|
|
|
|
|
|
return f"{size:.0f} {unit}"
|
|
|
|
|
|
size /= 1024
|
|
|
|
|
|
return f"{size:.0f} Po"
|
[IMP] qemu: a VM one can reach, and a first boot that does not stall
The monitor froze the address given at launch, so it lost the VM as soon as
cloud-init renamed the host and DHCP handed out another lease. It now
re-resolves at each attempt, in the views too, and reads virsh without sudo
— « sudo -n » fails in a detached session with no tty.
First boot also stopped paying for what it does not need: the guest agent
leaves cloud-init, snapd and locale-gen go, apt takes the fastest mirror.
The timezone follows the host. And when KVM is missing, the deployment says
so before the wait instead of being mysteriously fifteen times slower.
--- FR ---
Le suivi figeait l'adresse connue au lancement : il perdait donc la VM dès
que cloud-init posait le vrai nom d'hôte et que DHCP donnait un autre bail.
Il la ré-résout désormais à chaque tentative, dans les vues aussi, et lit
virsh sans sudo — « sudo -n » échoue dans une session détachée, sans tty.
Le premier démarrage cesse aussi de payer l'inutile : l'agent invité sort
de cloud-init, snapd et locale-gen disparaissent, apt prend le miroir le
plus rapide. Le fuseau suit l'hôte. Et faute de KVM, le déploiement le dit
avant l'attente, au lieu d'être quinze fois plus lent sans raison visible.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -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
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
# QEMU : déploiement d'un parc « infra ERPLibre »
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
ERPLIBRE_GIT_URL = "https://github.com/erplibre/erplibre"
|
[IMP] qemu: a VM one can reach, and a first boot that does not stall
The monitor froze the address given at launch, so it lost the VM as soon as
cloud-init renamed the host and DHCP handed out another lease. It now
re-resolves at each attempt, in the views too, and reads virsh without sudo
— « sudo -n » fails in a detached session with no tty.
First boot also stopped paying for what it does not need: the guest agent
leaves cloud-init, snapd and locale-gen go, apt takes the fastest mirror.
The timezone follows the host. And when KVM is missing, the deployment says
so before the wait instead of being mysteriously fifteen times slower.
--- FR ---
Le suivi figeait l'adresse connue au lancement : il perdait donc la VM dès
que cloud-init posait le vrai nom d'hôte et que DHCP donnait un autre bail.
Il la ré-résout désormais à chaque tentative, dans les vues aussi, et lit
virsh sans sudo — « sudo -n » échoue dans une session détachée, sans tty.
Le premier démarrage cesse aussi de payer l'inutile : l'agent invité sort
de cloud-init, snapd et locale-gen disparaissent, apt prend le miroir le
plus rapide. Le fuseau suit l'hôte. Et faute de KVM, le déploiement le dit
avant l'attente, au lieu d'être quinze fois plus lent sans raison visible.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -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
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _parse_disk_gb(size):
|
|
|
|
|
|
"""« 20G » -> 20 (Go, best effort)."""
|
|
|
|
|
|
m = re.match(r"\s*(\d+)", str(size))
|
|
|
|
|
|
return int(m.group(1)) if m else 0
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
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
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _is_yes(ans):
|
|
|
|
|
|
"""Réponse affirmative, FR et EN (o/oui/y/yes)."""
|
|
|
|
|
|
return ans.strip().lower() in ("y", "yes", "o", "oui")
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
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
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _is_yes_default_yes(ans):
|
|
|
|
|
|
"""Comme _is_yes mais le DÉFAUT (réponse vide) est OUI."""
|
|
|
|
|
|
a = ans.strip().lower()
|
|
|
|
|
|
return a == "" or a in ("y", "yes", "o", "oui")
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
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
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _is_no(ans):
|
|
|
|
|
|
"""Réponse négative explicite, FR et EN (n/no/non). Utile pour les
|
|
|
|
|
|
invites « défaut oui » où tout sauf « non » vaut oui."""
|
|
|
|
|
|
return ans.strip().lower() in ("n", "no", "non")
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
|
[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
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _host_disk_gb(path="/var/lib/libvirt/images"):
|
|
|
|
|
|
"""(libre, total) en Go du système de fichiers qui portera les disques.
|
|
|
|
|
|
|
|
|
|
|
|
On remonte vers le premier parent qui existe : le répertoire d'images
|
|
|
|
|
|
n'est créé qu'au premier déploiement, et « /var/lib/libvirt » ou « / »
|
|
|
|
|
|
répondent de la même partition dans la quasi-totalité des cas. (0, 0)
|
|
|
|
|
|
si rien ne répond — la place libre s'affiche alors comme inconnue
|
|
|
|
|
|
plutôt qu'inventée.
|
|
|
|
|
|
"""
|
|
|
|
|
|
chemin = path
|
|
|
|
|
|
while chemin and not os.path.isdir(chemin):
|
|
|
|
|
|
parent = os.path.dirname(chemin)
|
|
|
|
|
|
if parent == chemin:
|
|
|
|
|
|
break
|
|
|
|
|
|
chemin = parent
|
|
|
|
|
|
try:
|
|
|
|
|
|
usage = shutil.disk_usage(chemin or "/")
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
return 0, 0
|
|
|
|
|
|
return usage.free // (1 << 30), usage.total // (1 << 30)
|
|
|
|
|
|
|
[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
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _host_free_ram_mb():
|
|
|
|
|
|
"""RAM disponible de l'hôte en Mo (MemAvailable), 0 si inconnu."""
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
try:
|
[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
|
|
|
|
with open("/proc/meminfo") as fh:
|
|
|
|
|
|
for line in fh:
|
|
|
|
|
|
if line.startswith("MemAvailable:"):
|
|
|
|
|
|
return int(line.split()[1]) // 1024
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
return 0
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
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
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _is_index(token, options):
|
|
|
|
|
|
"""Vrai si le jeton est un numéro valide dans la liste (1-based)."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
return 1 <= int(token) <= len(options)
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
return False
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
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
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _parse_index_selection(raw, options):
|
|
|
|
|
|
"""« 1 3 » ou « 1,3 » -> sous-liste d'options (indices 1-based)."""
|
|
|
|
|
|
chosen = []
|
|
|
|
|
|
for tok in re.split(r"[\s,]+", raw.strip()):
|
|
|
|
|
|
if not tok:
|
|
|
|
|
|
continue
|
|
|
|
|
|
try:
|
|
|
|
|
|
idx = int(tok) - 1
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
if tok in options and tok not in chosen:
|
|
|
|
|
|
chosen.append(tok)
|
|
|
|
|
|
continue
|
|
|
|
|
|
if 0 <= idx < len(options) and options[idx] not in chosen:
|
|
|
|
|
|
chosen.append(options[idx])
|
|
|
|
|
|
return chosen
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
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
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _fmt_dur(secs):
|
|
|
|
|
|
"""Durée lisible : « 45s » ou « 2m05s »."""
|
|
|
|
|
|
secs = int(secs)
|
|
|
|
|
|
if secs < 60:
|
|
|
|
|
|
return f"{secs}s"
|
|
|
|
|
|
return f"{secs // 60}m{secs % 60:02d}s"
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
|
[ADD] release : publier 1.8.0, section de changelog et version déclarée
Les 39 puces du non-publié décrivent du travail fini qu'aucun numéro ne
permettait de citer. La section 1.8.0 les porte, avec la note de migration que
réclament le choix de l'interpréteur Python et le retrait d'Ubuntu 20.04 et
22.04. La version déclarée suit, comme le veut doc/RELEASE.base.md : le
fichier semver que lit env_var.sh, les huit pyproject, npm, CLAUDE.md et la
note des versions supportées. La ligne de contracts/compatibility.json reste
en 1.7.0, elle atteste un appariement mobile éprouvé et non la version
courante. Le commentaire de todo.py ne nomme plus de version, celle du dépôt
étant marquée « default » dans le fichier des versions supportées.
Vérifié : ERPLIBRE_VERSION=1.8.0, parité 17/8/10/2/2, tests de version verts.
--- EN ---
The unreleased list's 39 entries describe finished work no number could cite.
The 1.8.0 section carries them, with the migration note that choosing the
Python interpreter and dropping Ubuntu 20.04 and 22.04 call for. The declared
version follows, as doc/RELEASE.base.md wants: the semver file env_var.sh
reads, the eight pyproject files, npm, CLAUDE.md and the supported-version
note. The contracts/compatibility.json row stays at 1.7.0, attesting a tested
mobile pairing and not the current version. The todo.py comment names no
version any more, the repository's own being marked « default » in the
supported-version file. Checked: ERPLIBRE_VERSION=1.8.0, 17/8/10/2/2 parity,
version tests green.
Assisted-by: Claude Opus 5
2026-09-04 05:57:20 -04:00
|
|
|
|
# Cible d'installation Odoo exécutée dans la VM : la version que
|
|
|
|
|
|
# conf/supported_version_erplibre.json marque « default ».
|
[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
|
|
|
|
ERPLIBRE_ODOO_TARGET = "install_odoo_18"
|
|
|
|
|
|
# Go ajoutés au disque quand on installe ERPLibre (le minimum d'image ne
|
|
|
|
|
|
# laisse que ~97 Mo libres après l'installation).
|
|
|
|
|
|
ERPLIBRE_EXTRA_DISK_GB = 5
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
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
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _plural(word, count):
|
|
|
|
|
|
"""Accord simple : « échec » / « échecs ». Vaut pour fr et en."""
|
|
|
|
|
|
return word if abs(count) <= 1 else f"{word}s"
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
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
|
|
|
|
# Les trois invites ci-dessous sont posées à DEUX endroits — le profil
|
|
|
|
|
|
# global « Personnalisé » et la personnalisation par VM. Elles vivent donc
|
|
|
|
|
|
# ici : une seule définition, mêmes suggestions, mêmes validations.
|
|
|
|
|
|
# Chacune renvoie None pour « garder la valeur actuelle ».
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
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
|
|
|
|
def _confirm_or_discard(self, question):
|
|
|
|
|
|
"""Confirmation à défaut OUI, avec double validation sur le NON.
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
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
|
|
|
|
Un « non » distrait ferait perdre toutes les réponses déjà saisies. On
|
|
|
|
|
|
ne renonce donc que si l'abandon est confirmé ; sinon on repose la
|
|
|
|
|
|
question."""
|
|
|
|
|
|
while True:
|
|
|
|
|
|
if self._is_yes_default_yes(input(f"\n{question}")):
|
|
|
|
|
|
return True
|
|
|
|
|
|
if self._is_yes(
|
|
|
|
|
|
input(f" {t('Discard everything and start over? (y/N): ')}")
|
|
|
|
|
|
):
|
|
|
|
|
|
return False
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
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
|
|
|
|
# ---------------------------------------------------------------- #
|
|
|
|
|
|
# Déploiement : catalogue (pur) -> collecte (CLI ou TUI) -> exécution
|
|
|
|
|
|
# ---------------------------------------------------------------- #
|
2026-03-12 05:40:22 -04:00
|
|
|
|
|
|
|
|
|
|
def _deploy_clone_erplibre(self):
|
|
|
|
|
|
default_path = os.path.expanduser("~/erplibre")
|
|
|
|
|
|
target_path = (
|
2026-03-13 15:04:14 -04:00
|
|
|
|
input(t("Target directory path (default: ~/erplibre): ")).strip()
|
2026-03-12 05:40:22 -04:00
|
|
|
|
or default_path
|
|
|
|
|
|
)
|
|
|
|
|
|
target_path = os.path.expanduser(target_path)
|
|
|
|
|
|
if os.path.exists(target_path):
|
2026-03-13 15:04:14 -04:00
|
|
|
|
print(f"{t('Directory already exists: ')}{target_path}")
|
2026-03-12 05:40:22 -04:00
|
|
|
|
return
|
|
|
|
|
|
print(t("Cloning ERPLibre..."))
|
[ADD] déploiement : un proxy SOCKS par SSH, et son mode d'emploi
« -L » relaie UN service ; « -D » ouvre un relais SOCKS, par lequel le
navigateur atteint n'importe quelle destination depuis la machine distante —
une interface qui n'écoute que sur sa boucle locale, un hôte de son réseau sans
route depuis ici. Le port par défaut est 1080, modifiable, et contrôlé libre
avant d'ouvrir. La saisie de l'adresse est celle de sshfs, désormais partagée :
l'alias de ~/.ssh/config part tel quel à ssh, faute de quoi son ProxyJump se
perdrait et une VM imbriquée deviendrait injoignable. Le réglage de Firefox
s'affiche AVANT le lancement, la commande ne rendant la main qu'au Ctrl+C.
Vérifié : 9 tests neufs, et la numérotation du menu que deux autres gardent.
--- EN ---
"-L" relays ONE service; "-D" opens a SOCKS relay, through which the browser
reaches any destination from the remote machine — an interface listening only
on its loopback, a host of its network with no route from here. The default
port is 1080, changeable, and checked free before opening. The address prompt
is sshfs's, now shared: the ~/.ssh/config alias goes to ssh as is, failing
which its ProxyJump would be lost and a nested VM become unreachable. The
Firefox settings print BEFORE the launch, the command only returning on
Ctrl+C.
Checked: 9 new tests, and the menu numbering two others guard.
Assisted-by: Claude Opus 5
2026-09-24 13:32:28 -04:00
|
|
|
|
cmd = f"git clone https://github.com/erplibre/erplibre {target_path}"
|
2026-03-12 05:40:22 -04:00
|
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
|
|
|
|
|
try:
|
2026-03-13 15:04:14 -04:00
|
|
|
|
self.execute.exec_command_live(cmd, source_erplibre=False)
|
[ADD] déploiement : un proxy SOCKS par SSH, et son mode d'emploi
« -L » relaie UN service ; « -D » ouvre un relais SOCKS, par lequel le
navigateur atteint n'importe quelle destination depuis la machine distante —
une interface qui n'écoute que sur sa boucle locale, un hôte de son réseau sans
route depuis ici. Le port par défaut est 1080, modifiable, et contrôlé libre
avant d'ouvrir. La saisie de l'adresse est celle de sshfs, désormais partagée :
l'alias de ~/.ssh/config part tel quel à ssh, faute de quoi son ProxyJump se
perdrait et une VM imbriquée deviendrait injoignable. Le réglage de Firefox
s'affiche AVANT le lancement, la commande ne rendant la main qu'au Ctrl+C.
Vérifié : 9 tests neufs, et la numérotation du menu que deux autres gardent.
--- EN ---
"-L" relays ONE service; "-D" opens a SOCKS relay, through which the browser
reaches any destination from the remote machine — an interface listening only
on its loopback, a host of its network with no route from here. The default
port is 1080, changeable, and checked free before opening. The address prompt
is sshfs's, now shared: the ~/.ssh/config alias goes to ssh as is, failing
which its ProxyJump would be lost and a nested VM become unreachable. The
Firefox settings print BEFORE the launch, the command only returning on
Ctrl+C.
Checked: 9 new tests, and the menu numbering two others guard.
Assisted-by: Claude Opus 5
2026-09-24 13:32:28 -04:00
|
|
|
|
print(f"{t('ERPLibre cloned successfully to: ')}{target_path}")
|
2026-03-12 05:40:22 -04:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"{t('Error cloning ERPLibre: ')}{e}")
|
|
|
|
|
|
|
2026-04-11 00:13:56 -04:00
|
|
|
|
def _deploy_ntfy_server(self):
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"\n{t('Deploy a local NTFY push notification server (Ubuntu/Arch)')}"
|
|
|
|
|
|
)
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
port = input(t("NTFY server port (default: 8080): ")).strip() or "8080"
|
2026-04-11 00:13:56 -04:00
|
|
|
|
import socket
|
|
|
|
|
|
|
|
|
|
|
|
hostname = socket.gethostname()
|
|
|
|
|
|
try:
|
|
|
|
|
|
local_ip = socket.gethostbyname(hostname)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
local_ip = "127.0.0.1"
|
|
|
|
|
|
|
|
|
|
|
|
default_url = f"http://{local_ip}:{port}"
|
|
|
|
|
|
base_url = (
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
input(f"{t('NTFY base URL')} (default: {default_url}): ").strip()
|
2026-04-11 00:13:56 -04:00
|
|
|
|
or default_url
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
script_path = os.path.join(
|
|
|
|
|
|
os.path.dirname(os.path.abspath(__file__)),
|
|
|
|
|
|
"..",
|
|
|
|
|
|
"install",
|
|
|
|
|
|
"install_ntfy.sh",
|
|
|
|
|
|
)
|
|
|
|
|
|
script_path = os.path.realpath(script_path)
|
|
|
|
|
|
|
|
|
|
|
|
if not os.path.isfile(script_path):
|
|
|
|
|
|
print(f"{t('NTFY install script not found: ')}{script_path}")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
print(f"\n{t('Installing NTFY server (requires sudo)...')}")
|
|
|
|
|
|
cmd = (
|
|
|
|
|
|
f"sudo NTFY_PORT={port}"
|
|
|
|
|
|
f" NTFY_BASE_URL={base_url}"
|
|
|
|
|
|
f" bash {script_path}"
|
|
|
|
|
|
)
|
|
|
|
|
|
print(f"{t('Will execute:')} {cmd}\n")
|
|
|
|
|
|
try:
|
|
|
|
|
|
self.execute.exec_command_live(cmd, source_erplibre=False)
|
|
|
|
|
|
print(f"\n{t('NTFY server installed and started successfully!')}")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"{t('Error installing NTFY server: ')}{e}")
|
|
|
|
|
|
|
[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
|
|
|
|
def _deploy_qemu_cache(self):
|
|
|
|
|
|
"""Pose le miroir de téléchargement partagé par les VM QEMU de l'hôte.
|
|
|
|
|
|
|
|
|
|
|
|
Idempotente : l'installateur recompile, réécrit l'unité et redémarre le
|
|
|
|
|
|
service, qu'il existe déjà ou non.
|
|
|
|
|
|
|
|
|
|
|
|
Ce que l'entrée ANNONCE avant de demander sudo, et pourquoi : elle fait
|
|
|
|
|
|
écrire des règles sur le pont de l'hôte. Une règle trop large y prive la
|
|
|
|
|
|
machine de son propre réseau, et l'invite de sudo tombe entre deux
|
|
|
|
|
|
lignes de journal sans dire ce qu'elle sert à faire."""
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"\n{t('Install the download cache shared by the QEMU VMs of this host')}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
http_port = (
|
|
|
|
|
|
input(t("HTTP port of the cache (default: 8898): ")).strip()
|
|
|
|
|
|
or "8898"
|
|
|
|
|
|
)
|
|
|
|
|
|
tls_port = (
|
|
|
|
|
|
input(t("TLS port of the cache (default: 8899): ")).strip()
|
|
|
|
|
|
or "8899"
|
|
|
|
|
|
)
|
|
|
|
|
|
cache_dir = (
|
|
|
|
|
|
input(
|
|
|
|
|
|
t(
|
|
|
|
|
|
"Cache directory (default: /var/cache/erplibre_go_qemu_cache): "
|
|
|
|
|
|
)
|
|
|
|
|
|
).strip()
|
|
|
|
|
|
or "/var/cache/erplibre_go_qemu_cache"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
script_path = os.path.realpath(
|
|
|
|
|
|
os.path.join(
|
|
|
|
|
|
os.path.dirname(os.path.abspath(__file__)),
|
|
|
|
|
|
"..",
|
|
|
|
|
|
"install",
|
|
|
|
|
|
"install_qemu_cache.sh",
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
if not os.path.isfile(script_path):
|
|
|
|
|
|
print(f"{t('QEMU cache install script not found: ')}{script_path}")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
print(f"\n{t('Will write network rules on the host bridge:')}")
|
|
|
|
|
|
print(f" {t('Only what leaves the VM subnet is redirected')}")
|
|
|
|
|
|
print(f" {t('The rules exist only while the service runs')}")
|
|
|
|
|
|
if not shutil.which("go"):
|
|
|
|
|
|
print(f" {t('Go is absent; the installer lays it down')}")
|
|
|
|
|
|
print(
|
|
|
|
|
|
f" {t('No eviction is written: this cache never shrinks by itself')}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
cmd = (
|
|
|
|
|
|
f"sudo EL_HTTP_PORT={http_port}"
|
|
|
|
|
|
f" EL_TLS_PORT={tls_port}"
|
|
|
|
|
|
f" EL_CACHE_DIR={cache_dir}"
|
[ADD] cache qemu : messages du binaire en anglais ou en français
Le binaire ne parlait que français alors que le menu suit EL_LANG : un
opérateur anglophone lisait un statut qu'il ne comprenait pas. Chaque message
humain passe par T(), dont la clé EST le texte français, et un catalogue
anglais le traduit ; la langue vient de --lang, puis d'EL_LANG, le français
restant le défaut. Le menu demande le français pour ce qu'il RELIT, ses
motifs ne connaissant que cette langue. Un test refuse un message sans
traduction, une traduction orpheline et un verbe de format qui diverge.
--- EN ---
The binary spoke French only while the menu follows EL_LANG: an English
reader got a status he could not read. Every human message goes through T(),
whose key IS the French text, and an English catalogue translates it; the
language comes from --lang, then EL_LANG, French remaining the default. The
menu asks for French for what it READS BACK, its patterns knowing that
language alone. A test refuses a message without translation, an orphan
translation, and a format verb that drifts.
Assisted-by: Claude Opus 5
2026-09-16 02:59:01 -04:00
|
|
|
|
f" EL_LANG={get_lang()}"
|
[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
|
|
|
|
f" bash {script_path}"
|
|
|
|
|
|
)
|
|
|
|
|
|
print(f"\n{t('Will execute:')} {cmd}\n")
|
|
|
|
|
|
if not click.confirm(t("Install the QEMU download cache?")):
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"\n{t('Installing the QEMU download cache (requires sudo)...')}"
|
|
|
|
|
|
)
|
|
|
|
|
|
try:
|
|
|
|
|
|
code = self.execute.exec_command_live(cmd, source_erplibre=False)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"{t('The cache install failed, nothing is started')} : {e}")
|
|
|
|
|
|
return
|
|
|
|
|
|
# Le CODE de sortie, et pas seulement l'absence d'exception. Un
|
|
|
|
|
|
# installateur qui meurt — réseau libvirt absent, compilation qui
|
|
|
|
|
|
# cède — rend un code non nul sans rien lever, et l'entrée annonçait
|
|
|
|
|
|
# « installé et démarré » au-dessus de son propre message d'erreur.
|
|
|
|
|
|
# Rien n'est plus coûteux qu'un succès annoncé à tort : on cherche
|
|
|
|
|
|
# ensuite la panne partout sauf là où elle est.
|
|
|
|
|
|
if code:
|
|
|
|
|
|
print(f"\n ✗ {t('The cache install failed, nothing is started')}")
|
|
|
|
|
|
print(f" {t('Read the error above, fix it, and run entry 1.')}")
|
|
|
|
|
|
return
|
|
|
|
|
|
print(f"\n{t('QEMU download cache installed and started')}")
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"{t('Certificate authority a VM must trust: ')}"
|
|
|
|
|
|
"/var/lib/erplibre_go_qemu_cache/ca.crt"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _ssh_config_hosts():
|
|
|
|
|
|
"""Noms d'hôtes déclarés dans ~/.ssh/config, dans l'ordre du fichier.
|
|
|
|
|
|
|
[FIX] ssh config : une seule décision sur ce qu'est un nom de machine
Trois lecteurs de ~/.ssh/config tranchaient la même question autrement : l'un
comparait le mot-clé avec la casse, un autre exigeait un espace là où une
tabulation est légale, un troisième laissait passer le motif nié `!nom`. Un
alias déclaré « host » en minuscules était donc vu par l'un et invisible aux
deux autres, ce qui se lit comme une panne intermittente de la découverte.
La décision passe dans un module qui ne lit aucun fichier. Il rend None sur ce
qui n'est pas une déclaration et une liste — possiblement vide — sur ce qui en
est une : « Host * » ne nomme aucune machine mais reste une déclaration, sinon
ses directives se rattachent au bloc précédent. Vérifié : 19 tests, et le menu
reconnaît toujours le serveur du réseau par sa source SSH.
--- EN ---
Three readers of ~/.ssh/config settled the same question differently: one
compared the keyword case-sensitively, another required a space where a tab is
legal, a third let the negated pattern `!name` through. An alias declared with
a lowercase "host" was therefore seen by one and invisible to the other two,
which reads as intermittent discovery failure.
The decision moves into a module that reads no file. It returns None for what
is not a declaration and a list — possibly empty — for what is: "Host *" names
no machine yet remains a declaration, otherwise its directives attach to the
previous block. Checked: 19 tests, and the menu still recognises the server on
the network through its SSH source.
Assisted-by: Claude Opus 5
2026-09-09 04:39:01 -04:00
|
|
|
|
Une ligne « Host » peut porter plusieurs noms : on les rend tous.
|
|
|
|
|
|
Ce qui compte comme un nom de machine est tranché par
|
|
|
|
|
|
`ssh_config.declared_names`, en un seul endroit pour les trois
|
|
|
|
|
|
lecteurs de ce fichier — la casse du mot-clé, la tabulation qui
|
|
|
|
|
|
sépare et le motif nié s'y décidaient autrement dans chacun.
|
|
|
|
|
|
|
|
|
|
|
|
La lecture ne suit PAS `Include` : un alias déclaré dans un fichier
|
|
|
|
|
|
inclus reste invisible ici, alors même que `ssh -G` le résoudrait. La
|
|
|
|
|
|
source est donc incomplète sans être fausse."""
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
path = os.path.expanduser("~/.ssh/config")
|
|
|
|
|
|
names = []
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(path, encoding="utf-8") as fh:
|
|
|
|
|
|
for line in fh:
|
[FIX] ssh config : une seule décision sur ce qu'est un nom de machine
Trois lecteurs de ~/.ssh/config tranchaient la même question autrement : l'un
comparait le mot-clé avec la casse, un autre exigeait un espace là où une
tabulation est légale, un troisième laissait passer le motif nié `!nom`. Un
alias déclaré « host » en minuscules était donc vu par l'un et invisible aux
deux autres, ce qui se lit comme une panne intermittente de la découverte.
La décision passe dans un module qui ne lit aucun fichier. Il rend None sur ce
qui n'est pas une déclaration et une liste — possiblement vide — sur ce qui en
est une : « Host * » ne nomme aucune machine mais reste une déclaration, sinon
ses directives se rattachent au bloc précédent. Vérifié : 19 tests, et le menu
reconnaît toujours le serveur du réseau par sa source SSH.
--- EN ---
Three readers of ~/.ssh/config settled the same question differently: one
compared the keyword case-sensitively, another required a space where a tab is
legal, a third let the negated pattern `!name` through. An alias declared with
a lowercase "host" was therefore seen by one and invisible to the other two,
which reads as intermittent discovery failure.
The decision moves into a module that reads no file. It returns None for what
is not a declaration and a list — possibly empty — for what is: "Host *" names
no machine yet remains a declaration, otherwise its directives attach to the
previous block. Checked: 19 tests, and the menu still recognises the server on
the network through its SSH source.
Assisted-by: Claude Opus 5
2026-09-09 04:39:01 -04:00
|
|
|
|
for name in ssh_config.declared_names(line) or ():
|
|
|
|
|
|
if name not in names:
|
|
|
|
|
|
names.append(name)
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
except OSError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
return names
|
|
|
|
|
|
|
[FIX] proxmox : l'ancienne entrée ssh s'en va avec la convention
Le nom chaîné devient systématique, mais les entrées écrites AVANT portent le
nom court — et rien ne les retirerait : elles ne déclarent pas le nom qu'on
écrit maintenant. Deux blocs mèneraient à la même machine, exactement ce
qu'on venait d'enlever.
Le ProxyJump tranche : un bloc qui rebondit par CET hôte est le nôtre, on le
retire. Celui d'une VM locale homonyme n'en a pas, et on n'y touche jamais ;
celui d'un autre hôte Proxmox non plus.
Le drapeau Odoo gagne son test au passage. Il tombait pour la même raison que
les colonnes vides — la sonde est le dernier maillon de la suite distante, et
un parc où une seule VM n'a pas d'Odoo, un hyperviseur imbriqué par exemple,
finit en échec. Vérifié sur les trois VM : l'hôte rend bien « ODOO » pour les
deux qui écoutent, et le navigateur répondait 303 pendant que la colonne
disait « — ».
--- EN ---
The chained name becomes systematic, but entries written BEFORE carry the
short one — and nothing would retire them: they do not declare the name we
now write. Two blocks would lead to the same machine, exactly what we had
just removed.
The ProxyJump decides: a block hopping through THIS host is ours, so it goes.
A local namesake's has none, and is never touched; another Proxmox host's
neither.
The Odoo flag gains its test along the way. It failed for the same reason as
the empty columns — the probe is the remote pipeline's last link, and a fleet
where a single VM has no Odoo, a nested hypervisor for instance, ends in
failure. Verified on all three VMs: the host does return "ODOO" for the two
that listen, and the browser answered 303 while the column said "—".
Assisted-by: Claude Opus 5
2026-08-25 00:43:10 -04:00
|
|
|
|
@classmethod
|
|
|
|
|
|
def _ssh_config_block(cls, name):
|
|
|
|
|
|
"""Le bloc « Host … » qui déclare `name`, ou {}.
|
|
|
|
|
|
|
|
|
|
|
|
Rend ses noms ET ses directives : savoir qu'un nom est pris ne suffit
|
|
|
|
|
|
pas, il faut savoir PAR QUI. Le ProxyJump distingue notre propre
|
|
|
|
|
|
entrée — celle d'une VM derrière tel hôte — de celle d'une machine
|
|
|
|
|
|
qui se trouve porter le même nom.
|
|
|
|
|
|
|
|
|
|
|
|
{"names": [...], "proxyjump": "...", "hostname": "..."}."""
|
|
|
|
|
|
path = os.path.expanduser("~/.ssh/config")
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(path, encoding="utf-8") as fh:
|
|
|
|
|
|
contenu = fh.read()
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
return {}
|
|
|
|
|
|
bloc = None
|
|
|
|
|
|
for line in contenu.splitlines():
|
|
|
|
|
|
if re.match(r"^[ \t]*Host[ \t]+", line):
|
|
|
|
|
|
if bloc is not None:
|
|
|
|
|
|
return bloc
|
|
|
|
|
|
noms = line.split()[1:]
|
|
|
|
|
|
bloc = {"names": noms} if name in noms else None
|
|
|
|
|
|
continue
|
|
|
|
|
|
if bloc is None:
|
|
|
|
|
|
continue
|
|
|
|
|
|
# Une ligne non indentée et non vide clôt le bloc.
|
|
|
|
|
|
if line.strip() and not line[:1].isspace():
|
|
|
|
|
|
return bloc
|
|
|
|
|
|
mots = line.split()
|
|
|
|
|
|
if len(mots) >= 2 and mots[0].lower() in ("proxyjump", "hostname"):
|
|
|
|
|
|
bloc[mots[0].lower()] = mots[1]
|
|
|
|
|
|
return bloc or {}
|
|
|
|
|
|
|
[ADD] LongTest : jusqu'à quel étage un Proxmox imbriqué tient-il
La profondeur d'imbrication praticable ne se déduit pas, elle se mesure. Une
mesure à la main a trouvé, au quatrième étage, un invité 36 fois plus lent que
le temps réel — 583 secondes d'horloge pour 16 secondes de temps invité,
chaque ligne d'ACPI prenant une seconde — puis un noyau gelé au MÊME octet
quelles que soient les ressources. Un chiffre obtenu une fois, sur une
machine, n'est pas un chiffre.
D'où trois choses.
L'algorithme, en fonctions pures. Deux ressources s'épuisent en descendant :
la mémoire, chaque étage gardant de quoi faire tourner ses propres démons, et
le disque, celui de l'enfant vivant DANS celui du parent. Une troisième se
dégrade, et elle borne le vCPU à deux au-delà du premier étage : douze ont
gelé le noyau invité, les mêmes deux avançaient. La mémoire n'est PAS bornée —
la même VM gelait au même octet avec 9 Go et avec 2 Go, donc la rogner ne
gagnerait rien et priverait l'étage du dessous. Le plan est annoncé avant
toute création, et jamais au-delà de ce qui tient.
Le garde-fou dans l'écran. Il lisait la capacité de l'HÔTE et l'offrait en
entier : sur un troisième étage à 14 cœurs, il a proposé 12 vCPU à une VM qui
n'a jamais démarré. Le nombre n'était pas absurde pour la machine ; il l'était
pour sa profondeur, que l'écran ignorait. Elle se compte maintenant sur la
chaîne de ProxyJump — un rebond par étage, et c'est nous qui écrivons ces
entrées.
Le test long, dans LongTest/ et non dans test/ : le lanceur unitaire doit
rester lançable en quelques secondes, partout, y compris sans virtualisation.
La descente est uniforme — créer, attendre le ssh, installer, redémarrer et
vérifier le noyau, remettre pmxcfs debout, contrôler le stockage — et s'arrête
au premier étage qui échoue en NOMMANT l'étape. Il envoie notre
install_proxmox.sh par scp plutôt que de laisser la VM cloner le dépôt : c'est
notre code qu'on éprouve, et un correctif absent du distant a fait revenir le
même défaut sur trois VM.
--- EN ---
The practicable nesting depth cannot be deduced, only measured. A manual
measurement found, at the fourth level, a guest 36 times slower than real time
— 583 seconds of wall clock for 16 seconds of guest time, each ACPI line
taking a second — then a kernel frozen at the SAME byte whatever the
resources. A number obtained once, on one machine, is not a number.
Hence three things.
The algorithm, in pure functions. Two resources run out going down: memory,
each level keeping what its own daemons need, and disk, the child's living
INSIDE the parent's. A third degrades, and it caps the vCPU at two beyond the
first level: twelve froze the guest kernel, the same two progressed. Memory is
NOT capped — the same VM froze at the same byte with 9 GB and with 2 GB, so
trimming it would gain nothing and starve the level below. The plan is
announced before anything is created, and never beyond what fits.
The guard in the screen. It read the HOST's capacity and offered all of it: on
a third level with 14 cores it proposed 12 vCPU to a VM that never booted. The
number was not absurd for the machine; it was for its depth, which the screen
did not know. It is now counted on the ProxyJump chain — one hop per level,
and we are the ones writing those entries.
The long test, in LongTest/ and not test/: the unit runner must stay runnable
in seconds, anywhere, including without virtualisation. The descent is uniform
— create, wait for ssh, install, reboot and check the kernel, bring pmxcfs
back, check the storage — and stops at the first level that fails, NAMING the
step. It sends our install_proxmox.sh over scp instead of letting the VM clone
the repository: it is our code being exercised, and a fix absent from the
remote made the same defect return on three VMs.
Assisted-by: Claude Opus 5
(cherry picked from commit 4f70c461330cac6f46783a60e0f33052a979fa23)
2026-08-26 06:20:52 -04:00
|
|
|
|
@classmethod
|
|
|
|
|
|
def _ssh_jump_depth(cls, cible, maxi=12):
|
|
|
|
|
|
"""Nombre de rebonds pour joindre `cible`, en suivant la chaîne.
|
|
|
|
|
|
|
|
|
|
|
|
C'est la mesure de PROFONDEUR d'un hôte imbriqué, et la seule dont on
|
[ADD] assistant : serveur LLM reconnu, catalogue gpt, sessions locales
L'entrée « Question IA » envoyait toute question à une seule API distante, sur
un modèle figé et sans historique : aucun serveur local n'était atteignable,
et rien ne disait où partait le texte.
Deux règles gouvernent ce qui la remplace. Un port ne dit jamais qui répond —
un seul en héberge jusqu'à trois, et une famille réémet l'API d'une autre en
entier — donc l'identité se lit dans le CORPS, par une échelle ordonnée. Et
une adresse ne devient jamais du texte d'invite : le détecteur du dépôt ne
voit pas les noms d'hôte, d'où une poignée opaque à sa place.
Vérifié : 284 tests, et contre un serveur du réseau — reconnaissance,
capacités lues, conversation multi-tours, triage juste.
--- EN ---
The "AI question" entry sent every question to a single remote API, on a
frozen model and with no history: no local server was reachable, and nothing
said where the text was going.
Two rules govern what replaces it. A port never says who answers — one hosts
up to three products, and one family re-serves another's native API in full —
so identity is read from the BODY, by an ordered ladder. And an address never
becomes prompt text: the repository's detector does not see host names, hence
an opaque handle in its place.
Checked: 284 tests, and against a server on the network — recognition,
capabilities read, a multi-turn conversation, a correct triage.
Assisted-by: Claude Opus 5
2026-09-09 06:41:56 -04:00
|
|
|
|
dispose de l'extérieur. Elle est exacte pour les hôtes que cet outil
|
|
|
|
|
|
déploie : c'est lui qui écrit ces entrées, un ProxyJump par étage.
|
[ADD] LongTest : jusqu'à quel étage un Proxmox imbriqué tient-il
La profondeur d'imbrication praticable ne se déduit pas, elle se mesure. Une
mesure à la main a trouvé, au quatrième étage, un invité 36 fois plus lent que
le temps réel — 583 secondes d'horloge pour 16 secondes de temps invité,
chaque ligne d'ACPI prenant une seconde — puis un noyau gelé au MÊME octet
quelles que soient les ressources. Un chiffre obtenu une fois, sur une
machine, n'est pas un chiffre.
D'où trois choses.
L'algorithme, en fonctions pures. Deux ressources s'épuisent en descendant :
la mémoire, chaque étage gardant de quoi faire tourner ses propres démons, et
le disque, celui de l'enfant vivant DANS celui du parent. Une troisième se
dégrade, et elle borne le vCPU à deux au-delà du premier étage : douze ont
gelé le noyau invité, les mêmes deux avançaient. La mémoire n'est PAS bornée —
la même VM gelait au même octet avec 9 Go et avec 2 Go, donc la rogner ne
gagnerait rien et priverait l'étage du dessous. Le plan est annoncé avant
toute création, et jamais au-delà de ce qui tient.
Le garde-fou dans l'écran. Il lisait la capacité de l'HÔTE et l'offrait en
entier : sur un troisième étage à 14 cœurs, il a proposé 12 vCPU à une VM qui
n'a jamais démarré. Le nombre n'était pas absurde pour la machine ; il l'était
pour sa profondeur, que l'écran ignorait. Elle se compte maintenant sur la
chaîne de ProxyJump — un rebond par étage, et c'est nous qui écrivons ces
entrées.
Le test long, dans LongTest/ et non dans test/ : le lanceur unitaire doit
rester lançable en quelques secondes, partout, y compris sans virtualisation.
La descente est uniforme — créer, attendre le ssh, installer, redémarrer et
vérifier le noyau, remettre pmxcfs debout, contrôler le stockage — et s'arrête
au premier étage qui échoue en NOMMANT l'étape. Il envoie notre
install_proxmox.sh par scp plutôt que de laisser la VM cloner le dépôt : c'est
notre code qu'on éprouve, et un correctif absent du distant a fait revenir le
même défaut sur trois VM.
--- EN ---
The practicable nesting depth cannot be deduced, only measured. A manual
measurement found, at the fourth level, a guest 36 times slower than real time
— 583 seconds of wall clock for 16 seconds of guest time, each ACPI line
taking a second — then a kernel frozen at the SAME byte whatever the
resources. A number obtained once, on one machine, is not a number.
Hence three things.
The algorithm, in pure functions. Two resources run out going down: memory,
each level keeping what its own daemons need, and disk, the child's living
INSIDE the parent's. A third degrades, and it caps the vCPU at two beyond the
first level: twelve froze the guest kernel, the same two progressed. Memory is
NOT capped — the same VM froze at the same byte with 9 GB and with 2 GB, so
trimming it would gain nothing and starve the level below. The plan is
announced before anything is created, and never beyond what fits.
The guard in the screen. It read the HOST's capacity and offered all of it: on
a third level with 14 cores it proposed 12 vCPU to a VM that never booted. The
number was not absurd for the machine; it was for its depth, which the screen
did not know. It is now counted on the ProxyJump chain — one hop per level,
and we are the ones writing those entries.
The long test, in LongTest/ and not test/: the unit runner must stay runnable
in seconds, anywhere, including without virtualisation. The descent is uniform
— create, wait for ssh, install, reboot and check the kernel, bring pmxcfs
back, check the storage — and stops at the first level that fails, NAMING the
step. It sends our install_proxmox.sh over scp instead of letting the VM clone
the repository: it is our code being exercised, and a fix absent from the
remote made the same defect return on three VMs.
Assisted-by: Claude Opus 5
(cherry picked from commit 4f70c461330cac6f46783a60e0f33052a979fa23)
2026-08-26 06:20:52 -04:00
|
|
|
|
|
|
|
|
|
|
`maxi` borne le parcours : une boucle dans ~/.ssh/config — A qui
|
|
|
|
|
|
rebondit par B qui rebondit par A — tournerait sinon sans fin.
|
|
|
|
|
|
"""
|
|
|
|
|
|
vus, sauts = set(), 0
|
|
|
|
|
|
courant = cible
|
|
|
|
|
|
while sauts < maxi:
|
|
|
|
|
|
bloc = cls._ssh_config_block(courant)
|
|
|
|
|
|
saut = (bloc or {}).get("proxyjump")
|
|
|
|
|
|
if not saut or saut in vus:
|
|
|
|
|
|
break
|
|
|
|
|
|
vus.add(saut)
|
|
|
|
|
|
courant = saut
|
|
|
|
|
|
sauts += 1
|
|
|
|
|
|
return sauts
|
|
|
|
|
|
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _ssh_config_user(host):
|
|
|
|
|
|
"""`User` déclaré pour cet hôte dans ~/.ssh/config, ou "".
|
|
|
|
|
|
|
|
|
|
|
|
On suit la règle d'OpenSSH : le PREMIER `User` rencontré parmi les
|
|
|
|
|
|
blocs qui correspondent l'emporte, motifs (`Host *`) compris. Sans
|
|
|
|
|
|
déclaration on renvoie "" — il n'y a alors rien à copier, et
|
|
|
|
|
|
l'appelant garde son défaut plutôt que d'inventer le nom de session
|
|
|
|
|
|
locale."""
|
|
|
|
|
|
import fnmatch
|
|
|
|
|
|
|
|
|
|
|
|
path = os.path.expanduser("~/.ssh/config")
|
|
|
|
|
|
matching = False
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(path, encoding="utf-8") as fh:
|
|
|
|
|
|
for line in fh:
|
|
|
|
|
|
stripped = line.strip()
|
|
|
|
|
|
if re.match(r"^Host[ \t]+", stripped):
|
|
|
|
|
|
matching = any(
|
|
|
|
|
|
fnmatch.fnmatch(host, pattern)
|
|
|
|
|
|
for pattern in stripped.split()[1:]
|
|
|
|
|
|
)
|
|
|
|
|
|
continue
|
|
|
|
|
|
if matching:
|
|
|
|
|
|
found = re.match(
|
|
|
|
|
|
r"^User[ \t]+(\S+)", stripped, re.IGNORECASE
|
|
|
|
|
|
)
|
|
|
|
|
|
if found:
|
|
|
|
|
|
return found.group(1)
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _port_is_free(port):
|
|
|
|
|
|
"""Vrai si rien n'écoute sur ce port en local."""
|
|
|
|
|
|
import socket
|
|
|
|
|
|
|
|
|
|
|
|
with socket.socket() as sock:
|
|
|
|
|
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
|
|
|
|
try:
|
|
|
|
|
|
sock.bind(("127.0.0.1", int(port)))
|
|
|
|
|
|
return True
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _remote_port_open(host, port):
|
|
|
|
|
|
"""Quelqu'un écoute-t-il sur ce port DEPUIS l'hôte distant ?
|
|
|
|
|
|
|
|
|
|
|
|
True / False / None quand on n'a pas pu conclure (hôte injoignable,
|
|
|
|
|
|
pas de bash). On teste une vraie connexion TCP vers « localhost » et
|
|
|
|
|
|
non la table d'écoute : c'est exactement ce que fera le tunnel, y
|
|
|
|
|
|
compris le choix IPv4/IPv6 de la résolution.
|
|
|
|
|
|
"""
|
|
|
|
|
|
probe = (
|
|
|
|
|
|
f"exec 3<>/dev/tcp/localhost/{int(port)} && echo OPEN"
|
|
|
|
|
|
" || echo CLOSED"
|
|
|
|
|
|
)
|
|
|
|
|
|
try:
|
|
|
|
|
|
res = subprocess.run(
|
|
|
|
|
|
[
|
|
|
|
|
|
"ssh",
|
|
|
|
|
|
"-o",
|
|
|
|
|
|
"BatchMode=yes",
|
|
|
|
|
|
"-o",
|
|
|
|
|
|
"ConnectTimeout=10",
|
|
|
|
|
|
host,
|
|
|
|
|
|
f"bash -c {shlex.quote(probe)} 2>/dev/null",
|
|
|
|
|
|
],
|
|
|
|
|
|
capture_output=True,
|
|
|
|
|
|
text=True,
|
|
|
|
|
|
timeout=45,
|
|
|
|
|
|
)
|
|
|
|
|
|
except (OSError, subprocess.SubprocessError):
|
|
|
|
|
|
return None
|
|
|
|
|
|
out = res.stdout.strip()
|
|
|
|
|
|
if "OPEN" in out:
|
|
|
|
|
|
return True
|
|
|
|
|
|
if "CLOSED" in out:
|
|
|
|
|
|
return False
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def _deploy_port_forward(self):
|
|
|
|
|
|
"""Ouvre un tunnel SSH pour joindre un service distant depuis le
|
|
|
|
|
|
navigateur local.
|
|
|
|
|
|
|
|
|
|
|
|
Le port distant est vu DEPUIS la machine cible : « -L
|
|
|
|
|
|
local:localhost:distant ». Un rebond éventuel n'a pas à être indiqué —
|
|
|
|
|
|
le ProxyJump du bloc ~/.ssh/config s'applique tout seul, ce qui rend
|
|
|
|
|
|
joignable une VM imbriquée sans route directe."""
|
|
|
|
|
|
print(f"\n🔌 {t('SSH port forwarding')}")
|
|
|
|
|
|
hosts = self._ssh_config_hosts()
|
|
|
|
|
|
if hosts:
|
|
|
|
|
|
for i, name in enumerate(hosts, 1):
|
|
|
|
|
|
print(f" [{i}] {name}")
|
|
|
|
|
|
host = input(f"{t('Host (number or name):')} ").strip()
|
|
|
|
|
|
if not host:
|
|
|
|
|
|
print(t("Cancelled."))
|
|
|
|
|
|
return
|
|
|
|
|
|
if host.isdigit() and 1 <= int(host) <= len(hosts):
|
|
|
|
|
|
host = hosts[int(host) - 1]
|
|
|
|
|
|
|
|
|
|
|
|
raw = input(f"{t('Remote port (default:')} 8069): ").strip()
|
|
|
|
|
|
remote = raw if raw.isdigit() else "8069"
|
|
|
|
|
|
raw = input(f"{t('Local port (default:')} {remote}): ").strip()
|
|
|
|
|
|
local = raw if raw.isdigit() else remote
|
|
|
|
|
|
|
|
|
|
|
|
# Sonde AVANT d'ouvrir : sans elle, un service arrêté à l'autre bout
|
|
|
|
|
|
# ne se manifeste que par un mur de « channel N: open failed » à
|
|
|
|
|
|
# chaque requête du navigateur, qui ne dit pas d'où vient le refus.
|
|
|
|
|
|
print(f" {t('Checking the remote port...')}")
|
|
|
|
|
|
listening = self._remote_port_open(host, remote)
|
|
|
|
|
|
if listening is False:
|
|
|
|
|
|
print(
|
|
|
|
|
|
f" ⚠ {t('Nothing is listening on port')} {remote}"
|
|
|
|
|
|
f" {t('of')} {host}"
|
|
|
|
|
|
)
|
|
|
|
|
|
print(f" {t('Start the service there, or continue anyway.')}")
|
|
|
|
|
|
if not self._is_yes(input(t("Continue anyway? (y/N): "))):
|
|
|
|
|
|
return
|
|
|
|
|
|
elif listening is None:
|
|
|
|
|
|
print(f" ℹ️ {t('Could not probe the remote port; going on.')}")
|
|
|
|
|
|
|
|
|
|
|
|
if not self._port_is_free(local):
|
|
|
|
|
|
print(f" ⚠ {t('Local port already in use:')} {local}")
|
|
|
|
|
|
if not self._is_yes(input(t("Try anyway? (y/N): "))):
|
|
|
|
|
|
return
|
|
|
|
|
|
if local != remote:
|
|
|
|
|
|
# Odoo redirige d'après web.base.url : un port local différent
|
|
|
|
|
|
# renvoie le navigateur vers une adresse qui n'existe pas chez lui.
|
|
|
|
|
|
print(f" ⚠ {t('Local port differs from the remote one.')}")
|
|
|
|
|
|
print(f" {t('Odoo redirects using web.base.url; check it')}")
|
|
|
|
|
|
print(f" {t('matches http://localhost:')}{local}")
|
|
|
|
|
|
|
|
|
|
|
|
cmd = f"ssh -N -L {local}:localhost:{remote} {shlex.quote(host)}"
|
|
|
|
|
|
print(f"\n 🌐 http://localhost:{local}")
|
|
|
|
|
|
print(f" {t('Will execute:')} {cmd}")
|
|
|
|
|
|
print(f" {t('Ctrl+C closes the tunnel.')}\n")
|
|
|
|
|
|
try:
|
|
|
|
|
|
self.execute.exec_command_live(cmd, source_erplibre=False)
|
|
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
|
|
pass
|
|
|
|
|
|
print(f"\n {t('Tunnel closed.')}")
|
|
|
|
|
|
|
[FIX] sshfs : n'annoncer un montage que s'il a eu lieu
Après un code 1, le menu affichait « Monté sur … », la commande pour démonter
et celle pour ouvrir le répertoire : le montage n'avait pas eu lieu, et on
cherchait des fichiers dans un répertoire vide. Le succès ne s'affiche plus
qu'en cas de succès, et le point de montage inutilisé est retiré.
L'échec, lui, avait une cause : sshfs lit « a+b » comme un chaînage d'hôtes et
ne consulte jamais ~/.ssh/config pour l'alias entier. Or c'est todo.py qui
nomme les VM « rebond+domaine », et cette seconde moitié est un domaine
libvirt, pas un alias SSH du rebond. On résout donc l'alias par « ssh -G » et
on rend à sshfs une cible qu'il ne peut plus mal lire, ProxyJump comprise.
--- EN ---
After exit code 1, the menu still printed "Mounted on …", the unmount command
and the file-manager command: nothing had been mounted, and one went looking
for files in an empty directory. The success block now only prints on success,
and the unused mount point is removed.
The failure itself had a cause: sshfs reads "a+b" as host chaining and never
consults ~/.ssh/config for the whole alias. But todo.py is what names VMs
"jump+domain", and that second half is a libvirt domain, not an SSH alias on
the jump host. So we resolve the alias with "ssh -G" and hand sshfs a target
it can no longer misread, ProxyJump included.
Assisted-by: Claude Opus 5
2026-08-22 22:32:34 -04:00
|
|
|
|
# sshfs lit « a+b » comme un CHAÎNAGE d'hôtes — « ssh a, puis ssh b depuis
|
|
|
|
|
|
# a » — et ne consulte donc PAS ~/.ssh/config pour l'alias entier. Or c'est
|
|
|
|
|
|
# todo.py qui nomme les VM découvertes « jump+domaine » (voir la marche
|
|
|
|
|
|
# SSH) : ce sont les alias les plus utiles, et les seuls que sshfs échoue à
|
[ADD] assistant : serveur LLM reconnu, catalogue gpt, sessions locales
L'entrée « Question IA » envoyait toute question à une seule API distante, sur
un modèle figé et sans historique : aucun serveur local n'était atteignable,
et rien ne disait où partait le texte.
Deux règles gouvernent ce qui la remplace. Un port ne dit jamais qui répond —
un seul en héberge jusqu'à trois, et une famille réémet l'API d'une autre en
entier — donc l'identité se lit dans le CORPS, par une échelle ordonnée. Et
une adresse ne devient jamais du texte d'invite : le détecteur du dépôt ne
voit pas les noms d'hôte, d'où une poignée opaque à sa place.
Vérifié : 284 tests, et contre un serveur du réseau — reconnaissance,
capacités lues, conversation multi-tours, triage juste.
--- EN ---
The "AI question" entry sent every question to a single remote API, on a
frozen model and with no history: no local server was reachable, and nothing
said where the text was going.
Two rules govern what replaces it. A port never says who answers — one hosts
up to three products, and one family re-serves another's native API in full —
so identity is read from the BODY, by an ordered ladder. And an address never
becomes prompt text: the repository's detector does not see host names, hence
an opaque handle in its place.
Checked: 284 tests, and against a server on the network — recognition,
capabilities read, a multi-turn conversation, a correct triage.
Assisted-by: Claude Opus 5
2026-09-09 06:41:56 -04:00
|
|
|
|
# monter tel quel : le montage échoue, la seconde moitié du nom étant un
|
|
|
|
|
|
# domaine libvirt et non un alias SSH du rebond.
|
[FIX] sshfs : n'annoncer un montage que s'il a eu lieu
Après un code 1, le menu affichait « Monté sur … », la commande pour démonter
et celle pour ouvrir le répertoire : le montage n'avait pas eu lieu, et on
cherchait des fichiers dans un répertoire vide. Le succès ne s'affiche plus
qu'en cas de succès, et le point de montage inutilisé est retiré.
L'échec, lui, avait une cause : sshfs lit « a+b » comme un chaînage d'hôtes et
ne consulte jamais ~/.ssh/config pour l'alias entier. Or c'est todo.py qui
nomme les VM « rebond+domaine », et cette seconde moitié est un domaine
libvirt, pas un alias SSH du rebond. On résout donc l'alias par « ssh -G » et
on rend à sshfs une cible qu'il ne peut plus mal lire, ProxyJump comprise.
--- EN ---
After exit code 1, the menu still printed "Mounted on …", the unmount command
and the file-manager command: nothing had been mounted, and one went looking
for files in an empty directory. The success block now only prints on success,
and the unused mount point is removed.
The failure itself had a cause: sshfs reads "a+b" as host chaining and never
consults ~/.ssh/config for the whole alias. But todo.py is what names VMs
"jump+domain", and that second half is a libvirt domain, not an SSH alias on
the jump host. So we resolve the alias with "ssh -G" and hand sshfs a target
it can no longer misread, ProxyJump included.
Assisted-by: Claude Opus 5
2026-08-22 22:32:34 -04:00
|
|
|
|
SSHFS_CHAIN_SEP = "+"
|
|
|
|
|
|
|
|
|
|
|
|
# Options à rendre à sshfs quand on contourne l'alias : exactement celles
|
|
|
|
|
|
# que todo.py écrit dans l'entrée qu'il génère. Sans elles, une VM dont la
|
|
|
|
|
|
# clé d'hôte a changé — IP DHCP réutilisée — ferait échouer le montage.
|
|
|
|
|
|
SSH_FORWARD_OPTS = (
|
|
|
|
|
|
("port", "Port"),
|
|
|
|
|
|
("proxyjump", "ProxyJump"),
|
|
|
|
|
|
("identityfile", "IdentityFile"),
|
|
|
|
|
|
("identitiesonly", "IdentitiesOnly"),
|
|
|
|
|
|
("stricthostkeychecking", "StrictHostKeyChecking"),
|
|
|
|
|
|
("userknownhostsfile", "UserKnownHostsFile"),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Ce que dit stderr, et ce qu'il faut aller corriger. L'ordre compte : le
|
|
|
|
|
|
# premier motif trouvé gagne.
|
|
|
|
|
|
SSH_FAILURE_HINTS = (
|
|
|
|
|
|
("could not resolve hostname", "unknown host name: check HostName"),
|
|
|
|
|
|
("name or service not known", "unknown host name: check HostName"),
|
|
|
|
|
|
("connection timed out", "no answer: is the server up and reachable?"),
|
|
|
|
|
|
("operation timed out", "no answer: is the server up and reachable?"),
|
|
|
|
|
|
("no route to host", "no route: check the network or the ProxyJump"),
|
|
|
|
|
|
("connection refused", "nothing listening on the SSH port"),
|
|
|
|
|
|
("permission denied", "authentication refused: check User and key"),
|
|
|
|
|
|
("host key verification failed", "host key changed for this address"),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _ssh_config_entries(path):
|
|
|
|
|
|
"""[(alias, {hostname, user})] de ~/.ssh/config, dans l'ordre du fichier.
|
|
|
|
|
|
|
|
|
|
|
|
Pendant de `_ssh_config_hosts`, qui ne rend que les NOMS : ici le menu
|
|
|
|
|
|
de montage a besoin d'afficher aussi l'adresse et l'utilisateur.
|
|
|
|
|
|
|
|
|
|
|
|
« Host a b » déclare DEUX alias pour la même machine — c'est ce que
|
|
|
|
|
|
todo.py écrit lui-même quand une VM porte plusieurs noms. Les prendre
|
|
|
|
|
|
pour un seul nom donnait un alias « a b », que sshfs ne peut pas
|
[FIX] ssh config : une seule décision sur ce qu'est un nom de machine
Trois lecteurs de ~/.ssh/config tranchaient la même question autrement : l'un
comparait le mot-clé avec la casse, un autre exigeait un espace là où une
tabulation est légale, un troisième laissait passer le motif nié `!nom`. Un
alias déclaré « host » en minuscules était donc vu par l'un et invisible aux
deux autres, ce qui se lit comme une panne intermittente de la découverte.
La décision passe dans un module qui ne lit aucun fichier. Il rend None sur ce
qui n'est pas une déclaration et une liste — possiblement vide — sur ce qui en
est une : « Host * » ne nomme aucune machine mais reste une déclaration, sinon
ses directives se rattachent au bloc précédent. Vérifié : 19 tests, et le menu
reconnaît toujours le serveur du réseau par sa source SSH.
--- EN ---
Three readers of ~/.ssh/config settled the same question differently: one
compared the keyword case-sensitively, another required a space where a tab is
legal, a third let the negated pattern `!name` through. An alias declared with
a lowercase "host" was therefore seen by one and invisible to the other two,
which reads as intermittent discovery failure.
The decision moves into a module that reads no file. It returns None for what
is not a declaration and a list — possibly empty — for what is: "Host *" names
no machine yet remains a declaration, otherwise its directives attach to the
previous block. Checked: 19 tests, and the menu still recognises the server on
the network through its SSH source.
Assisted-by: Claude Opus 5
2026-09-09 04:39:01 -04:00
|
|
|
|
monter. Ce qui compte comme un nom de machine est tranché par
|
|
|
|
|
|
`ssh_config.declared_names`, partagé avec les deux autres lecteurs.
|
[FIX] sshfs : n'annoncer un montage que s'il a eu lieu
Après un code 1, le menu affichait « Monté sur … », la commande pour démonter
et celle pour ouvrir le répertoire : le montage n'avait pas eu lieu, et on
cherchait des fichiers dans un répertoire vide. Le succès ne s'affiche plus
qu'en cas de succès, et le point de montage inutilisé est retiré.
L'échec, lui, avait une cause : sshfs lit « a+b » comme un chaînage d'hôtes et
ne consulte jamais ~/.ssh/config pour l'alias entier. Or c'est todo.py qui
nomme les VM « rebond+domaine », et cette seconde moitié est un domaine
libvirt, pas un alias SSH du rebond. On résout donc l'alias par « ssh -G » et
on rend à sshfs une cible qu'il ne peut plus mal lire, ProxyJump comprise.
--- EN ---
After exit code 1, the menu still printed "Mounted on …", the unmount command
and the file-manager command: nothing had been mounted, and one went looking
for files in an empty directory. The success block now only prints on success,
and the unused mount point is removed.
The failure itself had a cause: sshfs reads "a+b" as host chaining and never
consults ~/.ssh/config for the whole alias. But todo.py is what names VMs
"jump+domain", and that second half is a libvirt domain, not an SSH alias on
the jump host. So we resolve the alias with "ssh -G" and hand sshfs a target
it can no longer misread, ProxyJump included.
Assisted-by: Claude Opus 5
2026-08-22 22:32:34 -04:00
|
|
|
|
"""
|
|
|
|
|
|
hosts = []
|
|
|
|
|
|
noms = []
|
|
|
|
|
|
info = {}
|
|
|
|
|
|
|
|
|
|
|
|
def clore():
|
|
|
|
|
|
for nom in noms:
|
|
|
|
|
|
hosts.append((nom, dict(info)))
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(path, encoding="utf-8", errors="replace") as fh:
|
|
|
|
|
|
lignes = fh.readlines()
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
return []
|
|
|
|
|
|
for ligne in lignes:
|
|
|
|
|
|
ligne = ligne.strip()
|
[FIX] ssh config : une seule décision sur ce qu'est un nom de machine
Trois lecteurs de ~/.ssh/config tranchaient la même question autrement : l'un
comparait le mot-clé avec la casse, un autre exigeait un espace là où une
tabulation est légale, un troisième laissait passer le motif nié `!nom`. Un
alias déclaré « host » en minuscules était donc vu par l'un et invisible aux
deux autres, ce qui se lit comme une panne intermittente de la découverte.
La décision passe dans un module qui ne lit aucun fichier. Il rend None sur ce
qui n'est pas une déclaration et une liste — possiblement vide — sur ce qui en
est une : « Host * » ne nomme aucune machine mais reste une déclaration, sinon
ses directives se rattachent au bloc précédent. Vérifié : 19 tests, et le menu
reconnaît toujours le serveur du réseau par sa source SSH.
--- EN ---
Three readers of ~/.ssh/config settled the same question differently: one
compared the keyword case-sensitively, another required a space where a tab is
legal, a third let the negated pattern `!name` through. An alias declared with
a lowercase "host" was therefore seen by one and invisible to the other two,
which reads as intermittent discovery failure.
The decision moves into a module that reads no file. It returns None for what
is not a declaration and a list — possibly empty — for what is: "Host *" names
no machine yet remains a declaration, otherwise its directives attach to the
previous block. Checked: 19 tests, and the menu still recognises the server on
the network through its SSH source.
Assisted-by: Claude Opus 5
2026-09-09 04:39:01 -04:00
|
|
|
|
declares = ssh_config.declared_names(ligne)
|
|
|
|
|
|
if declares is not None:
|
[FIX] sshfs : n'annoncer un montage que s'il a eu lieu
Après un code 1, le menu affichait « Monté sur … », la commande pour démonter
et celle pour ouvrir le répertoire : le montage n'avait pas eu lieu, et on
cherchait des fichiers dans un répertoire vide. Le succès ne s'affiche plus
qu'en cas de succès, et le point de montage inutilisé est retiré.
L'échec, lui, avait une cause : sshfs lit « a+b » comme un chaînage d'hôtes et
ne consulte jamais ~/.ssh/config pour l'alias entier. Or c'est todo.py qui
nomme les VM « rebond+domaine », et cette seconde moitié est un domaine
libvirt, pas un alias SSH du rebond. On résout donc l'alias par « ssh -G » et
on rend à sshfs une cible qu'il ne peut plus mal lire, ProxyJump comprise.
--- EN ---
After exit code 1, the menu still printed "Mounted on …", the unmount command
and the file-manager command: nothing had been mounted, and one went looking
for files in an empty directory. The success block now only prints on success,
and the unused mount point is removed.
The failure itself had a cause: sshfs reads "a+b" as host chaining and never
consults ~/.ssh/config for the whole alias. But todo.py is what names VMs
"jump+domain", and that second half is a libvirt domain, not an SSH alias on
the jump host. So we resolve the alias with "ssh -G" and hand sshfs a target
it can no longer misread, ProxyJump included.
Assisted-by: Claude Opus 5
2026-08-22 22:32:34 -04:00
|
|
|
|
clore()
|
[FIX] ssh config : une seule décision sur ce qu'est un nom de machine
Trois lecteurs de ~/.ssh/config tranchaient la même question autrement : l'un
comparait le mot-clé avec la casse, un autre exigeait un espace là où une
tabulation est légale, un troisième laissait passer le motif nié `!nom`. Un
alias déclaré « host » en minuscules était donc vu par l'un et invisible aux
deux autres, ce qui se lit comme une panne intermittente de la découverte.
La décision passe dans un module qui ne lit aucun fichier. Il rend None sur ce
qui n'est pas une déclaration et une liste — possiblement vide — sur ce qui en
est une : « Host * » ne nomme aucune machine mais reste une déclaration, sinon
ses directives se rattachent au bloc précédent. Vérifié : 19 tests, et le menu
reconnaît toujours le serveur du réseau par sa source SSH.
--- EN ---
Three readers of ~/.ssh/config settled the same question differently: one
compared the keyword case-sensitively, another required a space where a tab is
legal, a third let the negated pattern `!name` through. An alias declared with
a lowercase "host" was therefore seen by one and invisible to the other two,
which reads as intermittent discovery failure.
The decision moves into a module that reads no file. It returns None for what
is not a declaration and a list — possibly empty — for what is: "Host *" names
no machine yet remains a declaration, otherwise its directives attach to the
previous block. Checked: 19 tests, and the menu still recognises the server on
the network through its SSH source.
Assisted-by: Claude Opus 5
2026-09-09 04:39:01 -04:00
|
|
|
|
noms = declares
|
[FIX] sshfs : n'annoncer un montage que s'il a eu lieu
Après un code 1, le menu affichait « Monté sur … », la commande pour démonter
et celle pour ouvrir le répertoire : le montage n'avait pas eu lieu, et on
cherchait des fichiers dans un répertoire vide. Le succès ne s'affiche plus
qu'en cas de succès, et le point de montage inutilisé est retiré.
L'échec, lui, avait une cause : sshfs lit « a+b » comme un chaînage d'hôtes et
ne consulte jamais ~/.ssh/config pour l'alias entier. Or c'est todo.py qui
nomme les VM « rebond+domaine », et cette seconde moitié est un domaine
libvirt, pas un alias SSH du rebond. On résout donc l'alias par « ssh -G » et
on rend à sshfs une cible qu'il ne peut plus mal lire, ProxyJump comprise.
--- EN ---
After exit code 1, the menu still printed "Mounted on …", the unmount command
and the file-manager command: nothing had been mounted, and one went looking
for files in an empty directory. The success block now only prints on success,
and the unused mount point is removed.
The failure itself had a cause: sshfs reads "a+b" as host chaining and never
consults ~/.ssh/config for the whole alias. But todo.py is what names VMs
"jump+domain", and that second half is a libvirt domain, not an SSH alias on
the jump host. So we resolve the alias with "ssh -G" and hand sshfs a target
it can no longer misread, ProxyJump included.
Assisted-by: Claude Opus 5
2026-08-22 22:32:34 -04:00
|
|
|
|
info = {}
|
|
|
|
|
|
elif noms:
|
|
|
|
|
|
paire = ligne.split(None, 1)
|
|
|
|
|
|
if len(paire) == 2 and paire[0].lower() in (
|
|
|
|
|
|
"hostname",
|
|
|
|
|
|
"user",
|
|
|
|
|
|
):
|
|
|
|
|
|
info[paire[0].lower()] = paire[1].strip()
|
|
|
|
|
|
clore()
|
|
|
|
|
|
return hosts
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _ssh_resolve(alias):
|
|
|
|
|
|
"""Configuration RÉSOLUE de l'alias, telle que ssh la voit (ssh -G).
|
|
|
|
|
|
|
|
|
|
|
|
On délègue à ssh au lieu de relire le fichier : lui seul connaît les
|
|
|
|
|
|
Include, les Match, l'ordre des motifs et ses propres défauts.
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
res = subprocess.run(
|
|
|
|
|
|
["ssh", "-G", alias],
|
|
|
|
|
|
capture_output=True,
|
|
|
|
|
|
text=True,
|
|
|
|
|
|
timeout=15,
|
|
|
|
|
|
env=TODO._qemu_c_env(),
|
|
|
|
|
|
)
|
|
|
|
|
|
except (OSError, subprocess.SubprocessError):
|
|
|
|
|
|
return {}
|
|
|
|
|
|
if res.returncode != 0:
|
|
|
|
|
|
return {}
|
|
|
|
|
|
out = {}
|
|
|
|
|
|
for ligne in res.stdout.splitlines():
|
|
|
|
|
|
cle, _, val = ligne.strip().partition(" ")
|
|
|
|
|
|
# ssh -G répète « identityfile » : la PREMIÈRE est celle qui compte.
|
|
|
|
|
|
if cle and val and cle.lower() not in out:
|
|
|
|
|
|
out[cle.lower()] = val
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
def _sshfs_command(self, alias, mount_point, resolved=None):
|
|
|
|
|
|
"""(commande sshfs, alias contourné ?) pour monter cet alias.
|
|
|
|
|
|
|
|
|
|
|
|
Sans « + » dans le nom, on laisse sshfs faire : c'est ssh qui lit la
|
|
|
|
|
|
config, et rien ne vaut mieux. Avec un « + », on résout l'alias
|
|
|
|
|
|
soi-même et on rend à sshfs une cible qu'il ne peut plus mal lire.
|
|
|
|
|
|
"""
|
|
|
|
|
|
base = "sshfs -o follow_symlinks"
|
|
|
|
|
|
if self.SSHFS_CHAIN_SEP not in alias:
|
|
|
|
|
|
return f"{base} {alias}:/ {mount_point}", False
|
|
|
|
|
|
cfg = resolved if resolved is not None else self._ssh_resolve(alias)
|
|
|
|
|
|
host = cfg.get("hostname")
|
|
|
|
|
|
# Un hostname qui contient encore un « + » ne réglerait rien, et un
|
|
|
|
|
|
# alias non résolu vaut mieux qu'une cible inventée.
|
|
|
|
|
|
if not host or self.SSHFS_CHAIN_SEP in host:
|
|
|
|
|
|
return f"{base} {alias}:/ {mount_point}", False
|
|
|
|
|
|
opts = []
|
|
|
|
|
|
for cle, nom in self.SSH_FORWARD_OPTS:
|
|
|
|
|
|
val = cfg.get(cle)
|
|
|
|
|
|
if val and val.lower() != "none":
|
|
|
|
|
|
opts.append(f"-o {nom}={val}")
|
|
|
|
|
|
user = cfg.get("user")
|
|
|
|
|
|
cible = f"{user}@{host}" if user else host
|
|
|
|
|
|
pieces = [base] + opts + [f"{cible}:/", mount_point]
|
|
|
|
|
|
return " ".join(pieces), True
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _ssh_failure_hint(cls, stderr):
|
|
|
|
|
|
"""Première ligne utile de stderr, et ce qu'elle désigne."""
|
|
|
|
|
|
texte = (stderr or "").lower()
|
|
|
|
|
|
for motif, indice in cls.SSH_FAILURE_HINTS:
|
|
|
|
|
|
if motif in texte:
|
|
|
|
|
|
return indice
|
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _ssh_probe(alias, timeout=8):
|
|
|
|
|
|
"""(code, stderr) d'un « ssh <alias> true » sans invite de mot de passe.
|
|
|
|
|
|
|
|
|
|
|
|
BatchMode : une invite bloquerait le menu. Un refus d'authentification
|
|
|
|
|
|
se distingue donc d'un hôte injoignable, et le diagnostic le dit.
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
res = subprocess.run(
|
|
|
|
|
|
[
|
|
|
|
|
|
"ssh",
|
|
|
|
|
|
"-o",
|
|
|
|
|
|
"BatchMode=yes",
|
|
|
|
|
|
"-o",
|
|
|
|
|
|
f"ConnectTimeout={timeout}",
|
|
|
|
|
|
alias,
|
|
|
|
|
|
"true",
|
|
|
|
|
|
],
|
|
|
|
|
|
capture_output=True,
|
|
|
|
|
|
text=True,
|
|
|
|
|
|
timeout=timeout + 12,
|
|
|
|
|
|
env=TODO._qemu_c_env(),
|
|
|
|
|
|
)
|
|
|
|
|
|
except subprocess.TimeoutExpired:
|
|
|
|
|
|
return 255, "Connection timed out"
|
|
|
|
|
|
except (OSError, subprocess.SubprocessError) as exc:
|
|
|
|
|
|
return 255, str(exc)
|
|
|
|
|
|
return res.returncode, res.stderr.strip()
|
|
|
|
|
|
|
|
|
|
|
|
def _sshfs_diagnose(self, alias, mount_point, bypassed):
|
|
|
|
|
|
"""Dit POURQUOI le montage a échoué, et où aller corriger.
|
|
|
|
|
|
|
|
|
|
|
|
Le message est ciblé, pas une liste de causes possibles : on interroge
|
|
|
|
|
|
ssh, et selon qu'il passe ou non, le fautif n'est pas le même.
|
|
|
|
|
|
"""
|
|
|
|
|
|
print(f"\n ⚠ {t('sshfs mount failed.')}")
|
|
|
|
|
|
if not alias:
|
|
|
|
|
|
print(f" → {t('Check the SSH host and that the server is up.')}")
|
|
|
|
|
|
return
|
|
|
|
|
|
print(f" {t('Checking SSH access…')} ({alias})")
|
|
|
|
|
|
code, err = self._ssh_probe(alias)
|
|
|
|
|
|
if code == 0:
|
|
|
|
|
|
print(f" ✓ {t('SSH reaches this host: ~/.ssh/config is fine.')}")
|
|
|
|
|
|
if not bypassed and self.SSHFS_CHAIN_SEP in alias:
|
|
|
|
|
|
print(f" → {t('sshfs reads the « + » as host chaining.')}")
|
|
|
|
|
|
cmd, ok = self._sshfs_command(alias, mount_point)
|
|
|
|
|
|
if ok:
|
|
|
|
|
|
print(f" → {t('Run this instead:')}")
|
|
|
|
|
|
print(f" {cmd}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
# Annoncer une commande puis n'en donner aucune serait
|
|
|
|
|
|
# pire que se taire : on dit ce qui manque.
|
|
|
|
|
|
print(
|
|
|
|
|
|
f" → {t('ssh -G resolved nothing: check ~/.ssh/config.')}"
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f" → {t('Is sshfs (and fuse) installed here?')}")
|
|
|
|
|
|
return
|
|
|
|
|
|
indice = self._ssh_failure_hint(err)
|
|
|
|
|
|
if indice:
|
|
|
|
|
|
print(f" ✗ {t('SSH fails too:')} {t(indice)}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(
|
|
|
|
|
|
f" ✗ {t('SSH fails too:')} {err.splitlines()[0] if err else code}"
|
|
|
|
|
|
)
|
|
|
|
|
|
print(f" → {t('Update ~/.ssh/config, or check the server is up.')}")
|
|
|
|
|
|
|
[ADD] déploiement : un proxy SOCKS par SSH, et son mode d'emploi
« -L » relaie UN service ; « -D » ouvre un relais SOCKS, par lequel le
navigateur atteint n'importe quelle destination depuis la machine distante —
une interface qui n'écoute que sur sa boucle locale, un hôte de son réseau sans
route depuis ici. Le port par défaut est 1080, modifiable, et contrôlé libre
avant d'ouvrir. La saisie de l'adresse est celle de sshfs, désormais partagée :
l'alias de ~/.ssh/config part tel quel à ssh, faute de quoi son ProxyJump se
perdrait et une VM imbriquée deviendrait injoignable. Le réglage de Firefox
s'affiche AVANT le lancement, la commande ne rendant la main qu'au Ctrl+C.
Vérifié : 9 tests neufs, et la numérotation du menu que deux autres gardent.
--- EN ---
"-L" relays ONE service; "-D" opens a SOCKS relay, through which the browser
reaches any destination from the remote machine — an interface listening only
on its loopback, a host of its network with no route from here. The default
port is 1080, changeable, and checked free before opening. The address prompt
is sshfs's, now shared: the ~/.ssh/config alias goes to ssh as is, failing
which its ProxyJump would be lost and a nested VM become unreachable. The
Firefox settings print BEFORE the launch, the command only returning on
Ctrl+C.
Checked: 9 new tests, and the menu numbering two others guard.
Assisted-by: Claude Opus 5
2026-09-24 13:32:28 -04:00
|
|
|
|
def _ask_ssh_target(self):
|
|
|
|
|
|
"""Demande OÙ se connecter, à la main ou depuis ~/.ssh/config.
|
|
|
|
|
|
|
|
|
|
|
|
Rend (cible, utilisateur, hôte, nom, depuis_config), ou None si l'on
|
|
|
|
|
|
renonce. La CIBLE est ce qu'on passe à ssh : l'alias quand il vient du
|
|
|
|
|
|
fichier de configuration, pour que son User et son ProxyJump
|
|
|
|
|
|
s'appliquent — un « user@hôte » écrit à la main les perdrait, et une VM
|
|
|
|
|
|
imbriquée sans route directe deviendrait injoignable.
|
|
|
|
|
|
|
|
|
|
|
|
DEPUIS_CONFIG distingue les deux, que le nom seul ne sépare pas :
|
|
|
|
|
|
l'appelant n'interroge ~/.ssh/config que pour une adresse qui en vient.
|
|
|
|
|
|
"""
|
2026-03-22 22:28:24 -04:00
|
|
|
|
import getpass
|
|
|
|
|
|
|
|
|
|
|
|
print(f"\n{t('SSH address input method')}")
|
|
|
|
|
|
print(f"[1] {t('Manual entry')}")
|
|
|
|
|
|
print(f"[2] {t('From ~/.ssh/config')}")
|
|
|
|
|
|
choice = input(t("Your choice (1/2): ")).strip()
|
|
|
|
|
|
|
|
|
|
|
|
if choice == "2":
|
|
|
|
|
|
ssh_config_path = os.path.expanduser("~/.ssh/config")
|
[FIX] sshfs : n'annoncer un montage que s'il a eu lieu
Après un code 1, le menu affichait « Monté sur … », la commande pour démonter
et celle pour ouvrir le répertoire : le montage n'avait pas eu lieu, et on
cherchait des fichiers dans un répertoire vide. Le succès ne s'affiche plus
qu'en cas de succès, et le point de montage inutilisé est retiré.
L'échec, lui, avait une cause : sshfs lit « a+b » comme un chaînage d'hôtes et
ne consulte jamais ~/.ssh/config pour l'alias entier. Or c'est todo.py qui
nomme les VM « rebond+domaine », et cette seconde moitié est un domaine
libvirt, pas un alias SSH du rebond. On résout donc l'alias par « ssh -G » et
on rend à sshfs une cible qu'il ne peut plus mal lire, ProxyJump comprise.
--- EN ---
After exit code 1, the menu still printed "Mounted on …", the unmount command
and the file-manager command: nothing had been mounted, and one went looking
for files in an empty directory. The success block now only prints on success,
and the unused mount point is removed.
The failure itself had a cause: sshfs reads "a+b" as host chaining and never
consults ~/.ssh/config for the whole alias. But todo.py is what names VMs
"jump+domain", and that second half is a libvirt domain, not an SSH alias on
the jump host. So we resolve the alias with "ssh -G" and hand sshfs a target
it can no longer misread, ProxyJump included.
Assisted-by: Claude Opus 5
2026-08-22 22:32:34 -04:00
|
|
|
|
hosts = self._ssh_config_entries(ssh_config_path)
|
2026-03-22 22:28:24 -04:00
|
|
|
|
|
|
|
|
|
|
if not hosts:
|
|
|
|
|
|
print(t("No SSH hosts found in ~/.ssh/config"))
|
[ADD] déploiement : un proxy SOCKS par SSH, et son mode d'emploi
« -L » relaie UN service ; « -D » ouvre un relais SOCKS, par lequel le
navigateur atteint n'importe quelle destination depuis la machine distante —
une interface qui n'écoute que sur sa boucle locale, un hôte de son réseau sans
route depuis ici. Le port par défaut est 1080, modifiable, et contrôlé libre
avant d'ouvrir. La saisie de l'adresse est celle de sshfs, désormais partagée :
l'alias de ~/.ssh/config part tel quel à ssh, faute de quoi son ProxyJump se
perdrait et une VM imbriquée deviendrait injoignable. Le réglage de Firefox
s'affiche AVANT le lancement, la commande ne rendant la main qu'au Ctrl+C.
Vérifié : 9 tests neufs, et la numérotation du menu que deux autres gardent.
--- EN ---
"-L" relays ONE service; "-D" opens a SOCKS relay, through which the browser
reaches any destination from the remote machine — an interface listening only
on its loopback, a host of its network with no route from here. The default
port is 1080, changeable, and checked free before opening. The address prompt
is sshfs's, now shared: the ~/.ssh/config alias goes to ssh as is, failing
which its ProxyJump would be lost and a nested VM become unreachable. The
Firefox settings print BEFORE the launch, the command only returning on
Ctrl+C.
Checked: 9 new tests, and the menu numbering two others guard.
Assisted-by: Claude Opus 5
2026-09-24 13:32:28 -04:00
|
|
|
|
return None
|
2026-03-22 22:28:24 -04:00
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
|
for i, (host, info) in enumerate(hosts, 1):
|
|
|
|
|
|
hn = info.get("hostname", host)
|
|
|
|
|
|
u = info.get("user", "")
|
|
|
|
|
|
desc = host
|
|
|
|
|
|
if hn != host:
|
|
|
|
|
|
desc += f" ({hn})"
|
|
|
|
|
|
if u:
|
|
|
|
|
|
desc += f" [{u}]"
|
|
|
|
|
|
print(f"[{i}] {desc}")
|
|
|
|
|
|
sel = input(t("Select SSH host number: ")).strip()
|
|
|
|
|
|
try:
|
|
|
|
|
|
idx = int(sel) - 1
|
|
|
|
|
|
if idx < 0 or idx >= len(hosts):
|
|
|
|
|
|
print(t("Invalid selection!"))
|
[ADD] déploiement : un proxy SOCKS par SSH, et son mode d'emploi
« -L » relaie UN service ; « -D » ouvre un relais SOCKS, par lequel le
navigateur atteint n'importe quelle destination depuis la machine distante —
une interface qui n'écoute que sur sa boucle locale, un hôte de son réseau sans
route depuis ici. Le port par défaut est 1080, modifiable, et contrôlé libre
avant d'ouvrir. La saisie de l'adresse est celle de sshfs, désormais partagée :
l'alias de ~/.ssh/config part tel quel à ssh, faute de quoi son ProxyJump se
perdrait et une VM imbriquée deviendrait injoignable. Le réglage de Firefox
s'affiche AVANT le lancement, la commande ne rendant la main qu'au Ctrl+C.
Vérifié : 9 tests neufs, et la numérotation du menu que deux autres gardent.
--- EN ---
"-L" relays ONE service; "-D" opens a SOCKS relay, through which the browser
reaches any destination from the remote machine — an interface listening only
on its loopback, a host of its network with no route from here. The default
port is 1080, changeable, and checked free before opening. The address prompt
is sshfs's, now shared: the ~/.ssh/config alias goes to ssh as is, failing
which its ProxyJump would be lost and a nested VM become unreachable. The
Firefox settings print BEFORE the launch, the command only returning on
Ctrl+C.
Checked: 9 new tests, and the menu numbering two others guard.
Assisted-by: Claude Opus 5
2026-09-24 13:32:28 -04:00
|
|
|
|
return None
|
2026-03-22 22:28:24 -04:00
|
|
|
|
except ValueError:
|
|
|
|
|
|
print(t("Invalid selection!"))
|
[ADD] déploiement : un proxy SOCKS par SSH, et son mode d'emploi
« -L » relaie UN service ; « -D » ouvre un relais SOCKS, par lequel le
navigateur atteint n'importe quelle destination depuis la machine distante —
une interface qui n'écoute que sur sa boucle locale, un hôte de son réseau sans
route depuis ici. Le port par défaut est 1080, modifiable, et contrôlé libre
avant d'ouvrir. La saisie de l'adresse est celle de sshfs, désormais partagée :
l'alias de ~/.ssh/config part tel quel à ssh, faute de quoi son ProxyJump se
perdrait et une VM imbriquée deviendrait injoignable. Le réglage de Firefox
s'affiche AVANT le lancement, la commande ne rendant la main qu'au Ctrl+C.
Vérifié : 9 tests neufs, et la numérotation du menu que deux autres gardent.
--- EN ---
"-L" relays ONE service; "-D" opens a SOCKS relay, through which the browser
reaches any destination from the remote machine — an interface listening only
on its loopback, a host of its network with no route from here. The default
port is 1080, changeable, and checked free before opening. The address prompt
is sshfs's, now shared: the ~/.ssh/config alias goes to ssh as is, failing
which its ProxyJump would be lost and a nested VM become unreachable. The
Firefox settings print BEFORE the launch, the command only returning on
Ctrl+C.
Checked: 9 new tests, and the menu numbering two others guard.
Assisted-by: Claude Opus 5
2026-09-24 13:32:28 -04:00
|
|
|
|
return None
|
2026-03-22 22:28:24 -04:00
|
|
|
|
|
|
|
|
|
|
host_name, host_info = hosts[idx]
|
|
|
|
|
|
hostname = host_info.get("hostname", host_name)
|
|
|
|
|
|
user = host_info.get("user", getpass.getuser())
|
[ADD] déploiement : un proxy SOCKS par SSH, et son mode d'emploi
« -L » relaie UN service ; « -D » ouvre un relais SOCKS, par lequel le
navigateur atteint n'importe quelle destination depuis la machine distante —
une interface qui n'écoute que sur sa boucle locale, un hôte de son réseau sans
route depuis ici. Le port par défaut est 1080, modifiable, et contrôlé libre
avant d'ouvrir. La saisie de l'adresse est celle de sshfs, désormais partagée :
l'alias de ~/.ssh/config part tel quel à ssh, faute de quoi son ProxyJump se
perdrait et une VM imbriquée deviendrait injoignable. Le réglage de Firefox
s'affiche AVANT le lancement, la commande ne rendant la main qu'au Ctrl+C.
Vérifié : 9 tests neufs, et la numérotation du menu que deux autres gardent.
--- EN ---
"-L" relays ONE service; "-D" opens a SOCKS relay, through which the browser
reaches any destination from the remote machine — an interface listening only
on its loopback, a host of its network with no route from here. The default
port is 1080, changeable, and checked free before opening. The address prompt
is sshfs's, now shared: the ~/.ssh/config alias goes to ssh as is, failing
which its ProxyJump would be lost and a nested VM become unreachable. The
Firefox settings print BEFORE the launch, the command only returning on
Ctrl+C.
Checked: 9 new tests, and the menu numbering two others guard.
Assisted-by: Claude Opus 5
2026-09-24 13:32:28 -04:00
|
|
|
|
return host_name, user, hostname, host_name, True
|
|
|
|
|
|
|
|
|
|
|
|
ssh_host = input(t("SSH host (e.g.: user@192.168.1.100): ")).strip()
|
|
|
|
|
|
if not ssh_host:
|
|
|
|
|
|
print(t("SSH host is required!"))
|
|
|
|
|
|
return None
|
|
|
|
|
|
if "@" in ssh_host:
|
|
|
|
|
|
user, hostname = ssh_host.split("@", 1)
|
2026-03-22 22:28:24 -04:00
|
|
|
|
else:
|
[ADD] déploiement : un proxy SOCKS par SSH, et son mode d'emploi
« -L » relaie UN service ; « -D » ouvre un relais SOCKS, par lequel le
navigateur atteint n'importe quelle destination depuis la machine distante —
une interface qui n'écoute que sur sa boucle locale, un hôte de son réseau sans
route depuis ici. Le port par défaut est 1080, modifiable, et contrôlé libre
avant d'ouvrir. La saisie de l'adresse est celle de sshfs, désormais partagée :
l'alias de ~/.ssh/config part tel quel à ssh, faute de quoi son ProxyJump se
perdrait et une VM imbriquée deviendrait injoignable. Le réglage de Firefox
s'affiche AVANT le lancement, la commande ne rendant la main qu'au Ctrl+C.
Vérifié : 9 tests neufs, et la numérotation du menu que deux autres gardent.
--- EN ---
"-L" relays ONE service; "-D" opens a SOCKS relay, through which the browser
reaches any destination from the remote machine — an interface listening only
on its loopback, a host of its network with no route from here. The default
port is 1080, changeable, and checked free before opening. The address prompt
is sshfs's, now shared: the ~/.ssh/config alias goes to ssh as is, failing
which its ProxyJump would be lost and a nested VM become unreachable. The
Firefox settings print BEFORE the launch, the command only returning on
Ctrl+C.
Checked: 9 new tests, and the menu numbering two others guard.
Assisted-by: Claude Opus 5
2026-09-24 13:32:28 -04:00
|
|
|
|
hostname = ssh_host
|
|
|
|
|
|
user = getpass.getuser()
|
|
|
|
|
|
return f"{user}@{hostname}", user, hostname, hostname, False
|
|
|
|
|
|
|
|
|
|
|
|
def _deploy_socks_proxy(self):
|
|
|
|
|
|
"""Ouvre un proxy SOCKS qui fait sortir le navigateur PAR la machine
|
|
|
|
|
|
distante.
|
|
|
|
|
|
|
|
|
|
|
|
« -D » n'ouvre pas un tunnel vers UN service, comme « -L », mais un
|
|
|
|
|
|
relais SOCKS : le navigateur y envoie n'importe quelle destination, et
|
|
|
|
|
|
c'est la machine distante qui l'atteint. De quoi lire une interface
|
|
|
|
|
|
qui n'écoute que sur sa boucle locale, ou joindre un hôte de son
|
|
|
|
|
|
réseau sans route depuis ici.
|
|
|
|
|
|
|
|
|
|
|
|
« -N » n'ouvre aucun shell — rien à exécuter là-bas —, et « -C »
|
|
|
|
|
|
comprime, ce qui se sent sur une liaison lente.
|
|
|
|
|
|
"""
|
|
|
|
|
|
print(f"\n🧦 {t('SOCKS proxy over SSH')}")
|
|
|
|
|
|
choisi = self._ask_ssh_target()
|
|
|
|
|
|
if not choisi:
|
|
|
|
|
|
return
|
|
|
|
|
|
cible = choisi[0]
|
|
|
|
|
|
|
|
|
|
|
|
raw = input(f"{t('SOCKS port (default:')} 1080): ").strip()
|
|
|
|
|
|
port = raw if raw.isdigit() else "1080"
|
|
|
|
|
|
|
|
|
|
|
|
if not self._port_is_free(port):
|
|
|
|
|
|
print(f" ⚠ {t('Local port already in use:')} {port}")
|
|
|
|
|
|
if not self._is_yes(input(t("Try anyway? (y/N): "))):
|
2026-03-22 22:28:24 -04:00
|
|
|
|
return
|
[ADD] déploiement : un proxy SOCKS par SSH, et son mode d'emploi
« -L » relaie UN service ; « -D » ouvre un relais SOCKS, par lequel le
navigateur atteint n'importe quelle destination depuis la machine distante —
une interface qui n'écoute que sur sa boucle locale, un hôte de son réseau sans
route depuis ici. Le port par défaut est 1080, modifiable, et contrôlé libre
avant d'ouvrir. La saisie de l'adresse est celle de sshfs, désormais partagée :
l'alias de ~/.ssh/config part tel quel à ssh, faute de quoi son ProxyJump se
perdrait et une VM imbriquée deviendrait injoignable. Le réglage de Firefox
s'affiche AVANT le lancement, la commande ne rendant la main qu'au Ctrl+C.
Vérifié : 9 tests neufs, et la numérotation du menu que deux autres gardent.
--- EN ---
"-L" relays ONE service; "-D" opens a SOCKS relay, through which the browser
reaches any destination from the remote machine — an interface listening only
on its loopback, a host of its network with no route from here. The default
port is 1080, changeable, and checked free before opening. The address prompt
is sshfs's, now shared: the ~/.ssh/config alias goes to ssh as is, failing
which its ProxyJump would be lost and a nested VM become unreachable. The
Firefox settings print BEFORE the launch, the command only returning on
Ctrl+C.
Checked: 9 new tests, and the menu numbering two others guard.
Assisted-by: Claude Opus 5
2026-09-24 13:32:28 -04:00
|
|
|
|
|
|
|
|
|
|
cmd = f"ssh -D {port} -N -C {shlex.quote(cible)}"
|
|
|
|
|
|
print(f"\n {t('Will execute:')} {cmd}")
|
|
|
|
|
|
# Le mode d'emploi passe AVANT : la commande ne rend la main qu'au
|
|
|
|
|
|
# Ctrl+C, et c'est pendant qu'elle tourne qu'on règle le navigateur.
|
|
|
|
|
|
self._print_socks_help(port)
|
|
|
|
|
|
print(f" {t('Ctrl+C closes the tunnel.')}\n")
|
|
|
|
|
|
try:
|
|
|
|
|
|
self.execute.exec_command_live(cmd, source_erplibre=False)
|
|
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
|
|
pass
|
|
|
|
|
|
print(f"\n {t('Tunnel closed.')}")
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _print_socks_help(port):
|
|
|
|
|
|
"""Le réglage du navigateur, qu'aucune commande ne fait à sa place."""
|
|
|
|
|
|
# Les deux libellés qui portent des guillemets sortent de la
|
|
|
|
|
|
# f-string : les y laisser en réutiliserait le délimiteur, ce que
|
|
|
|
|
|
# Python n'accepte qu'à partir de 3.12.
|
|
|
|
|
|
choix = t('then choose "Manual proxy configuration":')
|
|
|
|
|
|
dns = t('Tick "Proxy DNS when using SOCKS v5"')
|
|
|
|
|
|
print(f"\n ── {t('Firefox configuration')} ──")
|
|
|
|
|
|
print(f" {t('Settings, then Network Settings and Settings...,')}")
|
|
|
|
|
|
print(f" {choix}\n")
|
|
|
|
|
|
print(f" {t('SOCKS host:')} 127.0.0.1, {t('port')} {port}")
|
|
|
|
|
|
print(f" {t('Tick SOCKS v5')}")
|
|
|
|
|
|
print(f" {dns}")
|
|
|
|
|
|
print(f"\n {t('Domain names are then resolved on the remote side,')}")
|
|
|
|
|
|
print(f" {t('which reaches internal names such as localhost, or')}")
|
|
|
|
|
|
print(f" {t('hosts of the remote network.')}\n")
|
|
|
|
|
|
|
|
|
|
|
|
def _configure_sshfs(self):
|
|
|
|
|
|
import re
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
|
|
choisi = self._ask_ssh_target()
|
|
|
|
|
|
if not choisi:
|
|
|
|
|
|
return
|
|
|
|
|
|
cible, user, hostname, ssh_name, depuis_config = choisi
|
|
|
|
|
|
target = f"{cible}:/"
|
2026-03-22 22:28:24 -04:00
|
|
|
|
|
|
|
|
|
|
safe_name = re.sub(r"[^a-zA-Z0-9_-]", "_", ssh_name)
|
|
|
|
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
|
|
|
|
mount_point = f"/tmp/sshfs_{safe_name}_{timestamp}"
|
|
|
|
|
|
os.makedirs(mount_point, exist_ok=True)
|
|
|
|
|
|
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
# -o follow_symlinks : sshfs résout les liens symboliques CÔTÉ SERVEUR.
|
|
|
|
|
|
# Indispensable pour les dépôts google-repo (ERPLibre) : leur .git est
|
|
|
|
|
|
# une chaîne de symlinks relatifs profonds (.repo/projects -> project-
|
|
|
|
|
|
# objects) que git ne peut pas traverser sur un montage sshfs par
|
|
|
|
|
|
# défaut (« erreur à la lecture de .git » -> git status/commit KO).
|
[FIX] sshfs : n'annoncer un montage que s'il a eu lieu
Après un code 1, le menu affichait « Monté sur … », la commande pour démonter
et celle pour ouvrir le répertoire : le montage n'avait pas eu lieu, et on
cherchait des fichiers dans un répertoire vide. Le succès ne s'affiche plus
qu'en cas de succès, et le point de montage inutilisé est retiré.
L'échec, lui, avait une cause : sshfs lit « a+b » comme un chaînage d'hôtes et
ne consulte jamais ~/.ssh/config pour l'alias entier. Or c'est todo.py qui
nomme les VM « rebond+domaine », et cette seconde moitié est un domaine
libvirt, pas un alias SSH du rebond. On résout donc l'alias par « ssh -G » et
on rend à sshfs une cible qu'il ne peut plus mal lire, ProxyJump comprise.
--- EN ---
After exit code 1, the menu still printed "Mounted on …", the unmount command
and the file-manager command: nothing had been mounted, and one went looking
for files in an empty directory. The success block now only prints on success,
and the unused mount point is removed.
The failure itself had a cause: sshfs reads "a+b" as host chaining and never
consults ~/.ssh/config for the whole alias. But todo.py is what names VMs
"jump+domain", and that second half is a libvirt domain, not an SSH alias on
the jump host. So we resolve the alias with "ssh -G" and hand sshfs a target
it can no longer misread, ProxyJump included.
Assisted-by: Claude Opus 5
2026-08-22 22:32:34 -04:00
|
|
|
|
# L'alias vient de ~/.ssh/config : c'est lui qui peut porter un « + »,
|
|
|
|
|
|
# et lui qu'on peut interroger en cas d'échec. Une saisie manuelle est
|
|
|
|
|
|
# rendue telle quelle — si elle contient un « + », c'est un chaînage
|
|
|
|
|
|
# demandé exprès.
|
[ADD] déploiement : un proxy SOCKS par SSH, et son mode d'emploi
« -L » relaie UN service ; « -D » ouvre un relais SOCKS, par lequel le
navigateur atteint n'importe quelle destination depuis la machine distante —
une interface qui n'écoute que sur sa boucle locale, un hôte de son réseau sans
route depuis ici. Le port par défaut est 1080, modifiable, et contrôlé libre
avant d'ouvrir. La saisie de l'adresse est celle de sshfs, désormais partagée :
l'alias de ~/.ssh/config part tel quel à ssh, faute de quoi son ProxyJump se
perdrait et une VM imbriquée deviendrait injoignable. Le réglage de Firefox
s'affiche AVANT le lancement, la commande ne rendant la main qu'au Ctrl+C.
Vérifié : 9 tests neufs, et la numérotation du menu que deux autres gardent.
--- EN ---
"-L" relays ONE service; "-D" opens a SOCKS relay, through which the browser
reaches any destination from the remote machine — an interface listening only
on its loopback, a host of its network with no route from here. The default
port is 1080, changeable, and checked free before opening. The address prompt
is sshfs's, now shared: the ~/.ssh/config alias goes to ssh as is, failing
which its ProxyJump would be lost and a nested VM become unreachable. The
Firefox settings print BEFORE the launch, the command only returning on
Ctrl+C.
Checked: 9 new tests, and the menu numbering two others guard.
Assisted-by: Claude Opus 5
2026-09-24 13:32:28 -04:00
|
|
|
|
alias = ssh_name if depuis_config else ""
|
[FIX] sshfs : n'annoncer un montage que s'il a eu lieu
Après un code 1, le menu affichait « Monté sur … », la commande pour démonter
et celle pour ouvrir le répertoire : le montage n'avait pas eu lieu, et on
cherchait des fichiers dans un répertoire vide. Le succès ne s'affiche plus
qu'en cas de succès, et le point de montage inutilisé est retiré.
L'échec, lui, avait une cause : sshfs lit « a+b » comme un chaînage d'hôtes et
ne consulte jamais ~/.ssh/config pour l'alias entier. Or c'est todo.py qui
nomme les VM « rebond+domaine », et cette seconde moitié est un domaine
libvirt, pas un alias SSH du rebond. On résout donc l'alias par « ssh -G » et
on rend à sshfs une cible qu'il ne peut plus mal lire, ProxyJump comprise.
--- EN ---
After exit code 1, the menu still printed "Mounted on …", the unmount command
and the file-manager command: nothing had been mounted, and one went looking
for files in an empty directory. The success block now only prints on success,
and the unused mount point is removed.
The failure itself had a cause: sshfs reads "a+b" as host chaining and never
consults ~/.ssh/config for the whole alias. But todo.py is what names VMs
"jump+domain", and that second half is a libvirt domain, not an SSH alias on
the jump host. So we resolve the alias with "ssh -G" and hand sshfs a target
it can no longer misread, ProxyJump included.
Assisted-by: Claude Opus 5
2026-08-22 22:32:34 -04:00
|
|
|
|
if alias:
|
|
|
|
|
|
cmd, bypassed = self._sshfs_command(alias, mount_point)
|
|
|
|
|
|
else:
|
|
|
|
|
|
cmd, bypassed = (
|
|
|
|
|
|
f"sshfs -o follow_symlinks {target} {mount_point}",
|
|
|
|
|
|
False,
|
|
|
|
|
|
)
|
2026-03-22 22:28:24 -04:00
|
|
|
|
print(f"{t('Mounting sshfs on: ')}{mount_point}")
|
|
|
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
|
|
|
|
|
try:
|
[FIX] sshfs : n'annoncer un montage que s'il a eu lieu
Après un code 1, le menu affichait « Monté sur … », la commande pour démonter
et celle pour ouvrir le répertoire : le montage n'avait pas eu lieu, et on
cherchait des fichiers dans un répertoire vide. Le succès ne s'affiche plus
qu'en cas de succès, et le point de montage inutilisé est retiré.
L'échec, lui, avait une cause : sshfs lit « a+b » comme un chaînage d'hôtes et
ne consulte jamais ~/.ssh/config pour l'alias entier. Or c'est todo.py qui
nomme les VM « rebond+domaine », et cette seconde moitié est un domaine
libvirt, pas un alias SSH du rebond. On résout donc l'alias par « ssh -G » et
on rend à sshfs une cible qu'il ne peut plus mal lire, ProxyJump comprise.
--- EN ---
After exit code 1, the menu still printed "Mounted on …", the unmount command
and the file-manager command: nothing had been mounted, and one went looking
for files in an empty directory. The success block now only prints on success,
and the unused mount point is removed.
The failure itself had a cause: sshfs reads "a+b" as host chaining and never
consults ~/.ssh/config for the whole alias. But todo.py is what names VMs
"jump+domain", and that second half is a libvirt domain, not an SSH alias on
the jump host. So we resolve the alias with "ssh -G" and hand sshfs a target
it can no longer misread, ProxyJump included.
Assisted-by: Claude Opus 5
2026-08-22 22:32:34 -04:00
|
|
|
|
status = self.execute.exec_command_live(cmd, source_erplibre=False)
|
2026-03-22 22:28:24 -04:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"{t('Error mounting sshfs: ')}{e}")
|
[FIX] sshfs : n'annoncer un montage que s'il a eu lieu
Après un code 1, le menu affichait « Monté sur … », la commande pour démonter
et celle pour ouvrir le répertoire : le montage n'avait pas eu lieu, et on
cherchait des fichiers dans un répertoire vide. Le succès ne s'affiche plus
qu'en cas de succès, et le point de montage inutilisé est retiré.
L'échec, lui, avait une cause : sshfs lit « a+b » comme un chaînage d'hôtes et
ne consulte jamais ~/.ssh/config pour l'alias entier. Or c'est todo.py qui
nomme les VM « rebond+domaine », et cette seconde moitié est un domaine
libvirt, pas un alias SSH du rebond. On résout donc l'alias par « ssh -G » et
on rend à sshfs une cible qu'il ne peut plus mal lire, ProxyJump comprise.
--- EN ---
After exit code 1, the menu still printed "Mounted on …", the unmount command
and the file-manager command: nothing had been mounted, and one went looking
for files in an empty directory. The success block now only prints on success,
and the unused mount point is removed.
The failure itself had a cause: sshfs reads "a+b" as host chaining and never
consults ~/.ssh/config for the whole alias. But todo.py is what names VMs
"jump+domain", and that second half is a libvirt domain, not an SSH alias on
the jump host. So we resolve the alias with "ssh -G" and hand sshfs a target
it can no longer misread, ProxyJump included.
Assisted-by: Claude Opus 5
2026-08-22 22:32:34 -04:00
|
|
|
|
status = 1
|
|
|
|
|
|
# Le reste ne s'affiche QUE si le montage a réussi : « Monté sur … »
|
|
|
|
|
|
# après un code 1 envoyait chercher des fichiers dans un répertoire
|
|
|
|
|
|
# vide, et faisait passer l'échec pour un détail.
|
|
|
|
|
|
if status:
|
|
|
|
|
|
self._sshfs_diagnose(alias, mount_point, bypassed)
|
|
|
|
|
|
# Le point de montage n'a jamais servi : le laisser accumulerait
|
|
|
|
|
|
# un répertoire vide dans /tmp à chaque tentative.
|
|
|
|
|
|
try:
|
|
|
|
|
|
os.rmdir(mount_point)
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
return
|
|
|
|
|
|
print(f"{t('Mounted on: ')}{mount_point}")
|
|
|
|
|
|
print("mount | grep sshfs")
|
[ADD] déploiement : un proxy SOCKS par SSH, et son mode d'emploi
« -L » relaie UN service ; « -D » ouvre un relais SOCKS, par lequel le
navigateur atteint n'importe quelle destination depuis la machine distante —
une interface qui n'écoute que sur sa boucle locale, un hôte de son réseau sans
route depuis ici. Le port par défaut est 1080, modifiable, et contrôlé libre
avant d'ouvrir. La saisie de l'adresse est celle de sshfs, désormais partagée :
l'alias de ~/.ssh/config part tel quel à ssh, faute de quoi son ProxyJump se
perdrait et une VM imbriquée deviendrait injoignable. Le réglage de Firefox
s'affiche AVANT le lancement, la commande ne rendant la main qu'au Ctrl+C.
Vérifié : 9 tests neufs, et la numérotation du menu que deux autres gardent.
--- EN ---
"-L" relays ONE service; "-D" opens a SOCKS relay, through which the browser
reaches any destination from the remote machine — an interface listening only
on its loopback, a host of its network with no route from here. The default
port is 1080, changeable, and checked free before opening. The address prompt
is sshfs's, now shared: the ~/.ssh/config alias goes to ssh as is, failing
which its ProxyJump would be lost and a nested VM become unreachable. The
Firefox settings print BEFORE the launch, the command only returning on
Ctrl+C.
Checked: 9 new tests, and the menu numbering two others guard.
Assisted-by: Claude Opus 5
2026-09-24 13:32:28 -04:00
|
|
|
|
print(f"{t('To unmount: ')}fusermount -u {mount_point}")
|
[FIX] sshfs : n'annoncer un montage que s'il a eu lieu
Après un code 1, le menu affichait « Monté sur … », la commande pour démonter
et celle pour ouvrir le répertoire : le montage n'avait pas eu lieu, et on
cherchait des fichiers dans un répertoire vide. Le succès ne s'affiche plus
qu'en cas de succès, et le point de montage inutilisé est retiré.
L'échec, lui, avait une cause : sshfs lit « a+b » comme un chaînage d'hôtes et
ne consulte jamais ~/.ssh/config pour l'alias entier. Or c'est todo.py qui
nomme les VM « rebond+domaine », et cette seconde moitié est un domaine
libvirt, pas un alias SSH du rebond. On résout donc l'alias par « ssh -G » et
on rend à sshfs une cible qu'il ne peut plus mal lire, ProxyJump comprise.
--- EN ---
After exit code 1, the menu still printed "Mounted on …", the unmount command
and the file-manager command: nothing had been mounted, and one went looking
for files in an empty directory. The success block now only prints on success,
and the unused mount point is removed.
The failure itself had a cause: sshfs reads "a+b" as host chaining and never
consults ~/.ssh/config for the whole alias. But todo.py is what names VMs
"jump+domain", and that second half is a libvirt domain, not an SSH alias on
the jump host. So we resolve the alias with "ssh -G" and hand sshfs a target
it can no longer misread, ProxyJump included.
Assisted-by: Claude Opus 5
2026-08-22 22:32:34 -04:00
|
|
|
|
print(f"nautilus {mount_point}/home/{user}")
|
2026-03-22 22:28:24 -04:00
|
|
|
|
|
[ADD] make: add SSH remote deployment targets and todo.py integration
Enable deploying and managing ERPLibre on remote servers via SSH
directly from make and the interactive todo.py CLI, since only
local deployment was previously supported.
- New conf/make.ssh.Makefile with 11 targets: ssh_check, ssh_push,
ssh_install, ssh_run, ssh_stop, ssh_restart, ssh_status, ssh_logs,
ssh_make, ssh_install_systemd, ssh_install_nginx
- Variables: SSH_HOST (required), SSH_USER, SSH_PORT, SSH_KEY,
SSH_PATH, SSH_TARGET, SSH_DOMAIN, SSH_ADMIN_EMAIL
- Execute > Deploy menu extended with 11 SSH options in todo.py
- All strings translated fr/en in todo_i18n.py
Generated by Claude Code 2.1.101 model claude-sonnet-4-6
Co-Authored-By: Mathieu Benoit <mathben@technolibre.ca>
2026-04-10 22:53:07 -04:00
|
|
|
|
def _get_ssh_params(self):
|
|
|
|
|
|
"""Prompt for SSH connection parameters. Returns dict or None on cancel."""
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
host = click.prompt(
|
[FIX] todo : une invite n'écrit plus son deux-points deux fois
`click.prompt` ajoute son propre « : » à l'étiquette qu'on lui donne, et
quinze étiquettes le portaient déjà. Le menu le plus vu du logiciel demandait
donc « Commande :: », et sept invites de déploiement à distance affichaient
un deux-points suivi d'un autre.
Le suffixe est désormais explicite partout où l'étiquette ponctue elle-même,
et sa valeur se déduit de l'étiquette : rien quand elle porte déjà l'espace
qui suit, un espace quand le deux-points est nu. Un contrôle lit l'arbre et
résout les DEUX langues, le français mettant une espace avant le deux-points
là où l'anglais n'en met pas — chercher dans le texte du code ne verrait que
la clé anglaise.
--- EN ---
`click.prompt` adds its own ":" to the label it is given, and fifteen labels
already carried one. The most-seen menu of the software therefore asked
"Command:: ", and seven remote-deployment prompts showed a colon followed by
another.
The suffix is now explicit wherever the label punctuates itself, and its value
follows from the label: nothing when it already carries the trailing space, a
space when the colon is bare. A check reads the tree and resolves BOTH
languages, French putting a space before the colon where English does not —
searching the code text would only see the English key.
Assisted-by: Claude Opus 5
2026-09-09 05:32:52 -04:00
|
|
|
|
t("Remote host (user@hostname or hostname): "), prompt_suffix=""
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
).strip()
|
[ADD] make: add SSH remote deployment targets and todo.py integration
Enable deploying and managing ERPLibre on remote servers via SSH
directly from make and the interactive todo.py CLI, since only
local deployment was previously supported.
- New conf/make.ssh.Makefile with 11 targets: ssh_check, ssh_push,
ssh_install, ssh_run, ssh_stop, ssh_restart, ssh_status, ssh_logs,
ssh_make, ssh_install_systemd, ssh_install_nginx
- Variables: SSH_HOST (required), SSH_USER, SSH_PORT, SSH_KEY,
SSH_PATH, SSH_TARGET, SSH_DOMAIN, SSH_ADMIN_EMAIL
- Execute > Deploy menu extended with 11 SSH options in todo.py
- All strings translated fr/en in todo_i18n.py
Generated by Claude Code 2.1.101 model claude-sonnet-4-6
Co-Authored-By: Mathieu Benoit <mathben@technolibre.ca>
2026-04-10 22:53:07 -04:00
|
|
|
|
if not host:
|
|
|
|
|
|
print(t("SSH host is required!"))
|
|
|
|
|
|
return None
|
|
|
|
|
|
user = (
|
[FIX] todo : une invite n'écrit plus son deux-points deux fois
`click.prompt` ajoute son propre « : » à l'étiquette qu'on lui donne, et
quinze étiquettes le portaient déjà. Le menu le plus vu du logiciel demandait
donc « Commande :: », et sept invites de déploiement à distance affichaient
un deux-points suivi d'un autre.
Le suffixe est désormais explicite partout où l'étiquette ponctue elle-même,
et sa valeur se déduit de l'étiquette : rien quand elle porte déjà l'espace
qui suit, un espace quand le deux-points est nu. Un contrôle lit l'arbre et
résout les DEUX langues, le français mettant une espace avant le deux-points
là où l'anglais n'en met pas — chercher dans le texte du code ne verrait que
la clé anglaise.
--- EN ---
`click.prompt` adds its own ":" to the label it is given, and fifteen labels
already carried one. The most-seen menu of the software therefore asked
"Command:: ", and seven remote-deployment prompts showed a colon followed by
another.
The suffix is now explicit wherever the label punctuates itself, and its value
follows from the label: nothing when it already carries the trailing space, a
space when the colon is bare. A check reads the tree and resolves BOTH
languages, French putting a space before the colon where English does not —
searching the code text would only see the English key.
Assisted-by: Claude Opus 5
2026-09-09 05:32:52 -04:00
|
|
|
|
click.prompt(
|
|
|
|
|
|
t("SSH user (default: erplibre): "), prompt_suffix=""
|
|
|
|
|
|
).strip()
|
[ADD] make: add SSH remote deployment targets and todo.py integration
Enable deploying and managing ERPLibre on remote servers via SSH
directly from make and the interactive todo.py CLI, since only
local deployment was previously supported.
- New conf/make.ssh.Makefile with 11 targets: ssh_check, ssh_push,
ssh_install, ssh_run, ssh_stop, ssh_restart, ssh_status, ssh_logs,
ssh_make, ssh_install_systemd, ssh_install_nginx
- Variables: SSH_HOST (required), SSH_USER, SSH_PORT, SSH_KEY,
SSH_PATH, SSH_TARGET, SSH_DOMAIN, SSH_ADMIN_EMAIL
- Execute > Deploy menu extended with 11 SSH options in todo.py
- All strings translated fr/en in todo_i18n.py
Generated by Claude Code 2.1.101 model claude-sonnet-4-6
Co-Authored-By: Mathieu Benoit <mathben@technolibre.ca>
2026-04-10 22:53:07 -04:00
|
|
|
|
or "erplibre"
|
|
|
|
|
|
)
|
[FIX] todo : une invite n'écrit plus son deux-points deux fois
`click.prompt` ajoute son propre « : » à l'étiquette qu'on lui donne, et
quinze étiquettes le portaient déjà. Le menu le plus vu du logiciel demandait
donc « Commande :: », et sept invites de déploiement à distance affichaient
un deux-points suivi d'un autre.
Le suffixe est désormais explicite partout où l'étiquette ponctue elle-même,
et sa valeur se déduit de l'étiquette : rien quand elle porte déjà l'espace
qui suit, un espace quand le deux-points est nu. Un contrôle lit l'arbre et
résout les DEUX langues, le français mettant une espace avant le deux-points
là où l'anglais n'en met pas — chercher dans le texte du code ne verrait que
la clé anglaise.
--- EN ---
`click.prompt` adds its own ":" to the label it is given, and fifteen labels
already carried one. The most-seen menu of the software therefore asked
"Command:: ", and seven remote-deployment prompts showed a colon followed by
another.
The suffix is now explicit wherever the label punctuates itself, and its value
follows from the label: nothing when it already carries the trailing space, a
space when the colon is bare. A check reads the tree and resolves BOTH
languages, French putting a space before the colon where English does not —
searching the code text would only see the English key.
Assisted-by: Claude Opus 5
2026-09-09 05:32:52 -04:00
|
|
|
|
port = (
|
|
|
|
|
|
click.prompt(
|
|
|
|
|
|
t("SSH port (default: 22): "), prompt_suffix=""
|
|
|
|
|
|
).strip()
|
|
|
|
|
|
or "22"
|
|
|
|
|
|
)
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
key = click.prompt(
|
[FIX] todo : une invite n'écrit plus son deux-points deux fois
`click.prompt` ajoute son propre « : » à l'étiquette qu'on lui donne, et
quinze étiquettes le portaient déjà. Le menu le plus vu du logiciel demandait
donc « Commande :: », et sept invites de déploiement à distance affichaient
un deux-points suivi d'un autre.
Le suffixe est désormais explicite partout où l'étiquette ponctue elle-même,
et sa valeur se déduit de l'étiquette : rien quand elle porte déjà l'espace
qui suit, un espace quand le deux-points est nu. Un contrôle lit l'arbre et
résout les DEUX langues, le français mettant une espace avant le deux-points
là où l'anglais n'en met pas — chercher dans le texte du code ne verrait que
la clé anglaise.
--- EN ---
`click.prompt` adds its own ":" to the label it is given, and fifteen labels
already carried one. The most-seen menu of the software therefore asked
"Command:: ", and seven remote-deployment prompts showed a colon followed by
another.
The suffix is now explicit wherever the label punctuates itself, and its value
follows from the label: nothing when it already carries the trailing space, a
space when the colon is bare. A check reads the tree and resolves BOTH
languages, French putting a space before the colon where English does not —
searching the code text would only see the English key.
Assisted-by: Claude Opus 5
2026-09-09 05:32:52 -04:00
|
|
|
|
t("SSH key path (default: ~/.ssh/id_rsa, empty for none): "),
|
|
|
|
|
|
prompt_suffix="",
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
).strip()
|
|
|
|
|
|
path = (
|
[ADD] make: add SSH remote deployment targets and todo.py integration
Enable deploying and managing ERPLibre on remote servers via SSH
directly from make and the interactive todo.py CLI, since only
local deployment was previously supported.
- New conf/make.ssh.Makefile with 11 targets: ssh_check, ssh_push,
ssh_install, ssh_run, ssh_stop, ssh_restart, ssh_status, ssh_logs,
ssh_make, ssh_install_systemd, ssh_install_nginx
- Variables: SSH_HOST (required), SSH_USER, SSH_PORT, SSH_KEY,
SSH_PATH, SSH_TARGET, SSH_DOMAIN, SSH_ADMIN_EMAIL
- Execute > Deploy menu extended with 11 SSH options in todo.py
- All strings translated fr/en in todo_i18n.py
Generated by Claude Code 2.1.101 model claude-sonnet-4-6
Co-Authored-By: Mathieu Benoit <mathben@technolibre.ca>
2026-04-10 22:53:07 -04:00
|
|
|
|
click.prompt(
|
[FIX] todo : une invite n'écrit plus son deux-points deux fois
`click.prompt` ajoute son propre « : » à l'étiquette qu'on lui donne, et
quinze étiquettes le portaient déjà. Le menu le plus vu du logiciel demandait
donc « Commande :: », et sept invites de déploiement à distance affichaient
un deux-points suivi d'un autre.
Le suffixe est désormais explicite partout où l'étiquette ponctue elle-même,
et sa valeur se déduit de l'étiquette : rien quand elle porte déjà l'espace
qui suit, un espace quand le deux-points est nu. Un contrôle lit l'arbre et
résout les DEUX langues, le français mettant une espace avant le deux-points
là où l'anglais n'en met pas — chercher dans le texte du code ne verrait que
la clé anglaise.
--- EN ---
`click.prompt` adds its own ":" to the label it is given, and fifteen labels
already carried one. The most-seen menu of the software therefore asked
"Command:: ", and seven remote-deployment prompts showed a colon followed by
another.
The suffix is now explicit wherever the label punctuates itself, and its value
follows from the label: nothing when it already carries the trailing space, a
space when the colon is bare. A check reads the tree and resolves BOTH
languages, French putting a space before the colon where English does not —
searching the code text would only see the English key.
Assisted-by: Claude Opus 5
2026-09-09 05:32:52 -04:00
|
|
|
|
t("Remote path (default: ~/erplibre_deploy_2): "),
|
|
|
|
|
|
prompt_suffix="",
|
[ADD] make: add SSH remote deployment targets and todo.py integration
Enable deploying and managing ERPLibre on remote servers via SSH
directly from make and the interactive todo.py CLI, since only
local deployment was previously supported.
- New conf/make.ssh.Makefile with 11 targets: ssh_check, ssh_push,
ssh_install, ssh_run, ssh_stop, ssh_restart, ssh_status, ssh_logs,
ssh_make, ssh_install_systemd, ssh_install_nginx
- Variables: SSH_HOST (required), SSH_USER, SSH_PORT, SSH_KEY,
SSH_PATH, SSH_TARGET, SSH_DOMAIN, SSH_ADMIN_EMAIL
- Execute > Deploy menu extended with 11 SSH options in todo.py
- All strings translated fr/en in todo_i18n.py
Generated by Claude Code 2.1.101 model claude-sonnet-4-6
Co-Authored-By: Mathieu Benoit <mathben@technolibre.ca>
2026-04-10 22:53:07 -04:00
|
|
|
|
).strip()
|
|
|
|
|
|
or "~/erplibre_deploy_2"
|
|
|
|
|
|
)
|
|
|
|
|
|
return {
|
|
|
|
|
|
"SSH_HOST": host,
|
|
|
|
|
|
"SSH_USER": user,
|
|
|
|
|
|
"SSH_PORT": port,
|
|
|
|
|
|
"SSH_KEY": key,
|
|
|
|
|
|
"SSH_PATH": path,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def _build_ssh_make_cmd(self, target, params, extra=None):
|
|
|
|
|
|
"""Build a make SSH command string from params dict."""
|
|
|
|
|
|
parts = [f"make {target}"]
|
|
|
|
|
|
for k, v in params.items():
|
|
|
|
|
|
if v:
|
|
|
|
|
|
parts.append(f'{k}="{v}"')
|
|
|
|
|
|
if extra:
|
|
|
|
|
|
for k, v in extra.items():
|
|
|
|
|
|
if v:
|
|
|
|
|
|
parts.append(f'{k}="{v}"')
|
|
|
|
|
|
return " ".join(parts)
|
|
|
|
|
|
|
|
|
|
|
|
def _deploy_ssh_check(self):
|
|
|
|
|
|
params = self._get_ssh_params()
|
|
|
|
|
|
if not params:
|
|
|
|
|
|
return
|
|
|
|
|
|
cmd = self._build_ssh_make_cmd("ssh_check", params)
|
|
|
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
|
cmd, source_erplibre=False, single_source_erplibre=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _deploy_ssh_push(self):
|
|
|
|
|
|
params = self._get_ssh_params()
|
|
|
|
|
|
if not params:
|
|
|
|
|
|
return
|
|
|
|
|
|
cmd = self._build_ssh_make_cmd("ssh_push", params)
|
|
|
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
|
cmd, source_erplibre=False, single_source_erplibre=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _deploy_ssh_install(self):
|
|
|
|
|
|
params = self._get_ssh_params()
|
|
|
|
|
|
if not params:
|
|
|
|
|
|
return
|
|
|
|
|
|
cmd = self._build_ssh_make_cmd("ssh_install", params)
|
|
|
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
|
cmd, source_erplibre=False, single_source_erplibre=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _deploy_ssh_run(self):
|
|
|
|
|
|
params = self._get_ssh_params()
|
|
|
|
|
|
if not params:
|
|
|
|
|
|
return
|
|
|
|
|
|
cmd = self._build_ssh_make_cmd("ssh_run", params)
|
|
|
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
|
cmd, source_erplibre=False, single_source_erplibre=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _deploy_ssh_stop(self):
|
|
|
|
|
|
params = self._get_ssh_params()
|
|
|
|
|
|
if not params:
|
|
|
|
|
|
return
|
|
|
|
|
|
cmd = self._build_ssh_make_cmd("ssh_stop", params)
|
|
|
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
|
cmd, source_erplibre=False, single_source_erplibre=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _deploy_ssh_restart(self):
|
|
|
|
|
|
params = self._get_ssh_params()
|
|
|
|
|
|
if not params:
|
|
|
|
|
|
return
|
|
|
|
|
|
cmd = self._build_ssh_make_cmd("ssh_restart", params)
|
|
|
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
|
cmd, source_erplibre=False, single_source_erplibre=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _deploy_ssh_status(self):
|
|
|
|
|
|
params = self._get_ssh_params()
|
|
|
|
|
|
if not params:
|
|
|
|
|
|
return
|
|
|
|
|
|
cmd = self._build_ssh_make_cmd("ssh_status", params)
|
|
|
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
|
cmd, source_erplibre=False, single_source_erplibre=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _deploy_ssh_logs(self):
|
|
|
|
|
|
params = self._get_ssh_params()
|
|
|
|
|
|
if not params:
|
|
|
|
|
|
return
|
|
|
|
|
|
cmd = self._build_ssh_make_cmd("ssh_logs", params)
|
|
|
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
|
cmd, source_erplibre=False, single_source_erplibre=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _deploy_ssh_make(self):
|
|
|
|
|
|
params = self._get_ssh_params()
|
|
|
|
|
|
if not params:
|
|
|
|
|
|
return
|
[FIX] todo : une invite n'écrit plus son deux-points deux fois
`click.prompt` ajoute son propre « : » à l'étiquette qu'on lui donne, et
quinze étiquettes le portaient déjà. Le menu le plus vu du logiciel demandait
donc « Commande :: », et sept invites de déploiement à distance affichaient
un deux-points suivi d'un autre.
Le suffixe est désormais explicite partout où l'étiquette ponctue elle-même,
et sa valeur se déduit de l'étiquette : rien quand elle porte déjà l'espace
qui suit, un espace quand le deux-points est nu. Un contrôle lit l'arbre et
résout les DEUX langues, le français mettant une espace avant le deux-points
là où l'anglais n'en met pas — chercher dans le texte du code ne verrait que
la clé anglaise.
--- EN ---
`click.prompt` adds its own ":" to the label it is given, and fifteen labels
already carried one. The most-seen menu of the software therefore asked
"Command:: ", and seven remote-deployment prompts showed a colon followed by
another.
The suffix is now explicit wherever the label punctuates itself, and its value
follows from the label: nothing when it already carries the trailing space, a
space when the colon is bare. A check reads the tree and resolves BOTH
languages, French putting a space before the colon where English does not —
searching the code text would only see the English key.
Assisted-by: Claude Opus 5
2026-09-09 05:32:52 -04:00
|
|
|
|
target = click.prompt(
|
|
|
|
|
|
t("Make target to run remotely: "), prompt_suffix=""
|
|
|
|
|
|
).strip()
|
[ADD] make: add SSH remote deployment targets and todo.py integration
Enable deploying and managing ERPLibre on remote servers via SSH
directly from make and the interactive todo.py CLI, since only
local deployment was previously supported.
- New conf/make.ssh.Makefile with 11 targets: ssh_check, ssh_push,
ssh_install, ssh_run, ssh_stop, ssh_restart, ssh_status, ssh_logs,
ssh_make, ssh_install_systemd, ssh_install_nginx
- Variables: SSH_HOST (required), SSH_USER, SSH_PORT, SSH_KEY,
SSH_PATH, SSH_TARGET, SSH_DOMAIN, SSH_ADMIN_EMAIL
- Execute > Deploy menu extended with 11 SSH options in todo.py
- All strings translated fr/en in todo_i18n.py
Generated by Claude Code 2.1.101 model claude-sonnet-4-6
Co-Authored-By: Mathieu Benoit <mathben@technolibre.ca>
2026-04-10 22:53:07 -04:00
|
|
|
|
if not target:
|
|
|
|
|
|
print(t("SSH host is required!"))
|
|
|
|
|
|
return
|
|
|
|
|
|
cmd = self._build_ssh_make_cmd(
|
|
|
|
|
|
"ssh_make", params, extra={"SSH_TARGET": target}
|
|
|
|
|
|
)
|
|
|
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
|
cmd, source_erplibre=False, single_source_erplibre=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _deploy_ssh_install_systemd(self):
|
|
|
|
|
|
params = self._get_ssh_params()
|
|
|
|
|
|
if not params:
|
|
|
|
|
|
return
|
|
|
|
|
|
cmd = self._build_ssh_make_cmd("ssh_install_systemd", params)
|
|
|
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
|
cmd, source_erplibre=False, single_source_erplibre=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _deploy_ssh_install_nginx(self):
|
|
|
|
|
|
params = self._get_ssh_params()
|
|
|
|
|
|
if not params:
|
|
|
|
|
|
return
|
[FIX] todo : une invite n'écrit plus son deux-points deux fois
`click.prompt` ajoute son propre « : » à l'étiquette qu'on lui donne, et
quinze étiquettes le portaient déjà. Le menu le plus vu du logiciel demandait
donc « Commande :: », et sept invites de déploiement à distance affichaient
un deux-points suivi d'un autre.
Le suffixe est désormais explicite partout où l'étiquette ponctue elle-même,
et sa valeur se déduit de l'étiquette : rien quand elle porte déjà l'espace
qui suit, un espace quand le deux-points est nu. Un contrôle lit l'arbre et
résout les DEUX langues, le français mettant une espace avant le deux-points
là où l'anglais n'en met pas — chercher dans le texte du code ne verrait que
la clé anglaise.
--- EN ---
`click.prompt` adds its own ":" to the label it is given, and fifteen labels
already carried one. The most-seen menu of the software therefore asked
"Command:: ", and seven remote-deployment prompts showed a colon followed by
another.
The suffix is now explicit wherever the label punctuates itself, and its value
follows from the label: nothing when it already carries the trailing space, a
space when the colon is bare. A check reads the tree and resolves BOTH
languages, French putting a space before the colon where English does not —
searching the code text would only see the English key.
Assisted-by: Claude Opus 5
2026-09-09 05:32:52 -04:00
|
|
|
|
domain = click.prompt(
|
|
|
|
|
|
t("Domain name (e.g.: example.com): "), prompt_suffix=""
|
|
|
|
|
|
).strip()
|
[ADD] make: add SSH remote deployment targets and todo.py integration
Enable deploying and managing ERPLibre on remote servers via SSH
directly from make and the interactive todo.py CLI, since only
local deployment was previously supported.
- New conf/make.ssh.Makefile with 11 targets: ssh_check, ssh_push,
ssh_install, ssh_run, ssh_stop, ssh_restart, ssh_status, ssh_logs,
ssh_make, ssh_install_systemd, ssh_install_nginx
- Variables: SSH_HOST (required), SSH_USER, SSH_PORT, SSH_KEY,
SSH_PATH, SSH_TARGET, SSH_DOMAIN, SSH_ADMIN_EMAIL
- Execute > Deploy menu extended with 11 SSH options in todo.py
- All strings translated fr/en in todo_i18n.py
Generated by Claude Code 2.1.101 model claude-sonnet-4-6
Co-Authored-By: Mathieu Benoit <mathben@technolibre.ca>
2026-04-10 22:53:07 -04:00
|
|
|
|
if not domain:
|
|
|
|
|
|
print(t("SSH host is required!"))
|
|
|
|
|
|
return
|
[FIX] todo : une invite n'écrit plus son deux-points deux fois
`click.prompt` ajoute son propre « : » à l'étiquette qu'on lui donne, et
quinze étiquettes le portaient déjà. Le menu le plus vu du logiciel demandait
donc « Commande :: », et sept invites de déploiement à distance affichaient
un deux-points suivi d'un autre.
Le suffixe est désormais explicite partout où l'étiquette ponctue elle-même,
et sa valeur se déduit de l'étiquette : rien quand elle porte déjà l'espace
qui suit, un espace quand le deux-points est nu. Un contrôle lit l'arbre et
résout les DEUX langues, le français mettant une espace avant le deux-points
là où l'anglais n'en met pas — chercher dans le texte du code ne verrait que
la clé anglaise.
--- EN ---
`click.prompt` adds its own ":" to the label it is given, and fifteen labels
already carried one. The most-seen menu of the software therefore asked
"Command:: ", and seven remote-deployment prompts showed a colon followed by
another.
The suffix is now explicit wherever the label punctuates itself, and its value
follows from the label: nothing when it already carries the trailing space, a
space when the colon is bare. A check reads the tree and resolves BOTH
languages, French putting a space before the colon where English does not —
searching the code text would only see the English key.
Assisted-by: Claude Opus 5
2026-09-09 05:32:52 -04:00
|
|
|
|
email = click.prompt(
|
|
|
|
|
|
t("Admin email for SSL certificate: "), prompt_suffix=""
|
|
|
|
|
|
).strip()
|
[ADD] make: add SSH remote deployment targets and todo.py integration
Enable deploying and managing ERPLibre on remote servers via SSH
directly from make and the interactive todo.py CLI, since only
local deployment was previously supported.
- New conf/make.ssh.Makefile with 11 targets: ssh_check, ssh_push,
ssh_install, ssh_run, ssh_stop, ssh_restart, ssh_status, ssh_logs,
ssh_make, ssh_install_systemd, ssh_install_nginx
- Variables: SSH_HOST (required), SSH_USER, SSH_PORT, SSH_KEY,
SSH_PATH, SSH_TARGET, SSH_DOMAIN, SSH_ADMIN_EMAIL
- Execute > Deploy menu extended with 11 SSH options in todo.py
- All strings translated fr/en in todo_i18n.py
Generated by Claude Code 2.1.101 model claude-sonnet-4-6
Co-Authored-By: Mathieu Benoit <mathben@technolibre.ca>
2026-04-10 22:53:07 -04:00
|
|
|
|
cmd = self._build_ssh_make_cmd(
|
|
|
|
|
|
"ssh_install_nginx",
|
|
|
|
|
|
params,
|
|
|
|
|
|
extra={"SSH_DOMAIN": domain, "SSH_ADMIN_EMAIL": email},
|
|
|
|
|
|
)
|
|
|
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
|
cmd, source_erplibre=False, single_source_erplibre=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-08-07 03:22:26 -04:00
|
|
|
|
def prompt_execute_code(self):
|
|
|
|
|
|
print(f"🤖 {t('What do you need for development?')}")
|
|
|
|
|
|
# help_info = """Commande :
|
|
|
|
|
|
# [1] Status Git local et distant
|
|
|
|
|
|
# [2] Démarrer le générateur de code
|
|
|
|
|
|
# [3] Format - Formatage automatique selon changement [ou manuelle]
|
|
|
|
|
|
# [4] Qualité - Qualité logiciel, détecter les fichiers qui manquent les licences AGPLv3
|
|
|
|
|
|
# [0] Retour
|
|
|
|
|
|
# """
|
|
|
|
|
|
# help_info = """Commande :
|
|
|
|
|
|
# [1] Status Git local et distant
|
|
|
|
|
|
# [0] Retour
|
|
|
|
|
|
# """
|
|
|
|
|
|
|
|
|
|
|
|
choices = self.config_file.get_config("code_from_makefile")
|
|
|
|
|
|
|
|
|
|
|
|
menu_entry = {
|
|
|
|
|
|
"prompt_description": t("Open SHELL"),
|
|
|
|
|
|
}
|
|
|
|
|
|
choices.append(menu_entry)
|
|
|
|
|
|
|
|
|
|
|
|
menu_entry = {
|
|
|
|
|
|
"prompt_description": t("Upgrade Module"),
|
|
|
|
|
|
}
|
|
|
|
|
|
choices.append(menu_entry)
|
|
|
|
|
|
|
|
|
|
|
|
choices.append(
|
|
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t("Debug"),
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Déplacé depuis le menu Execute : mise à jour de tout le code source
|
|
|
|
|
|
# de dev en staging (sous-menu de mise à jour).
|
|
|
|
|
|
choices.append(
|
|
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
|
"Update - Update all developed staging source code"
|
|
|
|
|
|
),
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
help_info = self.fill_help_info(choices)
|
|
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
|
print()
|
|
|
|
|
|
if status == "0":
|
|
|
|
|
|
return False
|
|
|
|
|
|
elif status == str(len(choices)):
|
|
|
|
|
|
self.prompt_execute_update()
|
|
|
|
|
|
elif status == str(len(choices) - 1):
|
|
|
|
|
|
self.debug_ide()
|
|
|
|
|
|
elif status == str(len(choices) - 2):
|
|
|
|
|
|
self.upgrade_module()
|
|
|
|
|
|
elif status == str(len(choices) - 3):
|
|
|
|
|
|
self.open_shell_on_database()
|
|
|
|
|
|
else:
|
|
|
|
|
|
cmd_no_found = True
|
|
|
|
|
|
try:
|
|
|
|
|
|
int_cmd = int(status)
|
|
|
|
|
|
if 0 < int_cmd <= len(choices):
|
|
|
|
|
|
cmd_no_found = False
|
|
|
|
|
|
instance = choices[int_cmd - 1]
|
|
|
|
|
|
self.execute_from_configuration(instance)
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
if cmd_no_found:
|
|
|
|
|
|
print(t("Command not found !"))
|
|
|
|
|
|
|
2026-08-31 06:17:48 -04:00
|
|
|
|
# Les hooks que le dépôt fournit. git saute silencieusement un hook qui
|
|
|
|
|
|
# ne porte pas le bit d'exécution, d'où la vérification à l'installation.
|
|
|
|
|
|
_GIT_HOOKS = ("commit-msg", "pre-commit")
|
|
|
|
|
|
_GIT_HOOKS_PATH = os.path.join("script", "git", "hooks")
|
|
|
|
|
|
|
2026-03-09 16:34:43 -04:00
|
|
|
|
def prompt_execute_git(self):
|
2026-09-02 04:43:25 -04:00
|
|
|
|
print(f"🤖 {t('Git and shell management tools!')}")
|
2026-03-10 03:45:11 -04:00
|
|
|
|
choices = [
|
2026-03-12 05:31:45 -04:00
|
|
|
|
{"prompt_description": t("Local git server")},
|
|
|
|
|
|
{"prompt_description": t("Add a remote to a local repository")},
|
2026-08-31 06:17:48 -04:00
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
|
"Install git hooks (commit-msg, pre-commit)"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
2026-09-02 04:01:10 -04:00
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
|
"Set merge.conflictStyle to zdiff3 (global)"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
2026-03-09 16:34:43 -04:00
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
# Append config-driven entries
|
2026-03-10 03:45:11 -04:00
|
|
|
|
config_entries = self.config_file.get_config("git_from_makefile")
|
|
|
|
|
|
if config_entries:
|
|
|
|
|
|
choices.extend(config_entries)
|
2026-03-09 16:34:43 -04:00
|
|
|
|
|
2026-09-02 04:43:25 -04:00
|
|
|
|
# Starship ferme la liste : c'est un outil de shell, pas de git. Son
|
|
|
|
|
|
# rang dépend du nombre d'entrées venues de todo.json, donc « method »
|
|
|
|
|
|
# porte la destination dans l'entrée elle-même — un numéro codé en dur
|
|
|
|
|
|
# mènerait ailleurs dès qu'une entrée de configuration s'ajoute.
|
|
|
|
|
|
choices.append(
|
|
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t("Install Starship on Shell"),
|
|
|
|
|
|
"method": "_shell_install_starship",
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
[ADD] script todo : installer Claude Code et opencode, PATH garanti
Ces installateurs posent leur binaire dans un répertoire du HOME que le PATH
d'un shell ne porte pas toujours : sans la ligne d'export, le binaire est là
et la commande reste introuvable. Le répertoire diffère d'un outil à l'autre,
et l'un des deux écrit déjà sa propre ligne — la présence se teste donc sur
le répertoire, pas sur la graphie, et rien n'est ajouté deux fois.
Le PATH d'un processus est figé depuis son démarrage : un nouveau shell est
nécessaire, ce que la sortie dit. Troisième écrivain dans le fichier de
shell, starship compris, d'où l'écriture mise en commun. Vérifié : 31 tests.
--- EN ---
These installers put their binary in a HOME directory that a shell's PATH
does not always carry: without the export line, the binary is there and the
command stays not found. The directory differs from one tool to the other,
and one of the two already writes its own line — presence is therefore tested
on the directory, not on the spelling, and nothing is added twice.
A process PATH is frozen since its start: a new shell is needed, which the
output says. Third writer into the shell file, starship included, hence the
shared write. Checked: 31 tests.
Assisted-by: Claude Opus 5
2026-09-02 04:51:03 -04:00
|
|
|
|
choices.append(
|
|
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t("Install Claude Code"),
|
|
|
|
|
|
"method": "_shell_install_claude_code",
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
choices.append(
|
|
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t("Install opencode"),
|
|
|
|
|
|
"method": "_shell_install_opencode",
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
2026-09-02 04:43:25 -04:00
|
|
|
|
|
2026-03-10 03:45:11 -04:00
|
|
|
|
help_info = self.fill_help_info(choices)
|
2026-03-09 16:34:43 -04:00
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
|
print()
|
|
|
|
|
|
if status == "0":
|
|
|
|
|
|
return False
|
|
|
|
|
|
elif status == "1":
|
|
|
|
|
|
self.prompt_execute_git_local_server()
|
2026-03-12 04:04:52 -04:00
|
|
|
|
elif status == "2":
|
|
|
|
|
|
self._git_add_remote()
|
2026-08-31 06:17:48 -04:00
|
|
|
|
elif status == "3":
|
|
|
|
|
|
self._git_install_hooks()
|
2026-09-02 04:01:10 -04:00
|
|
|
|
elif status == "4":
|
|
|
|
|
|
self._git_set_conflict_style()
|
2026-03-09 16:34:43 -04:00
|
|
|
|
else:
|
|
|
|
|
|
cmd_no_found = True
|
|
|
|
|
|
try:
|
|
|
|
|
|
int_cmd = int(status)
|
2026-03-10 03:45:11 -04:00
|
|
|
|
if 0 < int_cmd <= len(choices):
|
2026-03-09 16:34:43 -04:00
|
|
|
|
cmd_no_found = False
|
2026-03-10 03:45:11 -04:00
|
|
|
|
instance = choices[int_cmd - 1]
|
2026-09-02 04:43:25 -04:00
|
|
|
|
method = instance.get("method")
|
|
|
|
|
|
if method:
|
|
|
|
|
|
getattr(self, method)()
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.execute_from_configuration(instance)
|
2026-03-09 16:34:43 -04:00
|
|
|
|
except ValueError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
if cmd_no_found:
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(t("Command not found !"))
|
2026-03-09 16:34:43 -04:00
|
|
|
|
|
2026-03-12 04:04:52 -04:00
|
|
|
|
def _git_add_remote(self):
|
|
|
|
|
|
remote_name = (
|
2026-03-13 15:04:14 -04:00
|
|
|
|
input(t("Remote name (default: localhost): ")).strip()
|
|
|
|
|
|
or "localhost"
|
2026-03-12 04:04:52 -04:00
|
|
|
|
)
|
2026-03-13 15:04:14 -04:00
|
|
|
|
remote_url = input(
|
|
|
|
|
|
t("Repository address (e.g.: git://192.168.1.100/my-repo.git): ")
|
|
|
|
|
|
).strip()
|
2026-03-12 04:04:52 -04:00
|
|
|
|
if not remote_url:
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(t("Repository address is required!"))
|
2026-03-12 04:04:52 -04:00
|
|
|
|
return
|
|
|
|
|
|
cmd = f"git remote add {remote_name} {remote_url}"
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
2026-03-12 04:04:52 -04:00
|
|
|
|
try:
|
2026-03-13 15:04:14 -04:00
|
|
|
|
self.execute.exec_command_live(cmd, source_erplibre=False)
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(t("Remote added successfully!"))
|
2026-03-12 04:04:52 -04:00
|
|
|
|
except Exception as e:
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(f"{t('Error adding remote: ')}{e}")
|
2026-03-12 04:04:52 -04:00
|
|
|
|
|
2026-08-31 06:17:48 -04:00
|
|
|
|
def _git_install_hooks(self):
|
|
|
|
|
|
"""Pointer core.hooksPath sur les hooks du dépôt.
|
|
|
|
|
|
|
|
|
|
|
|
Le bit d'exécution fait partie de l'installation : sans lui git
|
|
|
|
|
|
ignore le hook sans rien dire, et le garde-fou du message de commit
|
|
|
|
|
|
passe inaperçu.
|
|
|
|
|
|
"""
|
|
|
|
|
|
racine = self._claude_context_root()
|
|
|
|
|
|
absolu = os.path.join(racine, self._GIT_HOOKS_PATH)
|
|
|
|
|
|
if not os.path.isdir(absolu):
|
|
|
|
|
|
print(f"{t('Hooks directory is missing: ')}{absolu}")
|
|
|
|
|
|
return
|
|
|
|
|
|
actuel = self._git_hooks_path(racine)
|
|
|
|
|
|
if actuel and actuel != self._GIT_HOOKS_PATH:
|
|
|
|
|
|
print(f"{t('Another hooks path is already set: ')}{actuel}")
|
|
|
|
|
|
if not self._is_yes(input(t("Replace it? (y/Y): "))):
|
|
|
|
|
|
print(t("Nothing to do."))
|
|
|
|
|
|
return
|
|
|
|
|
|
for hook in self._GIT_HOOKS:
|
|
|
|
|
|
chemin = os.path.join(absolu, hook)
|
|
|
|
|
|
if os.path.isfile(chemin) and not os.access(chemin, os.X_OK):
|
|
|
|
|
|
os.chmod(chemin, os.stat(chemin).st_mode | 0o111)
|
|
|
|
|
|
print(f"{t('Execution bit added: ')}{hook}")
|
|
|
|
|
|
# « -C racine » et non le cwd : lancé depuis un dépôt imbriqué
|
|
|
|
|
|
# (odoo18.0/addons/…), git écrirait core.hooksPath là-bas et la
|
|
|
|
|
|
# racine resterait sans garde-fou, sans le moindre message.
|
|
|
|
|
|
cmd = (
|
|
|
|
|
|
f"git -C {shlex.quote(racine)} config"
|
|
|
|
|
|
f" core.hooksPath {self._GIT_HOOKS_PATH}"
|
|
|
|
|
|
)
|
|
|
|
|
|
print(f"{t('Will execute:')} {cmd}")
|
|
|
|
|
|
# exec_command_live RETOURNE le code de sortie, il ne lève rien : sans
|
|
|
|
|
|
# ce test, un « fatal: not in a git directory » annonçait quand même
|
|
|
|
|
|
# « Hooks git installés! ». Le rapport qui suit ne rattrape pas, il
|
|
|
|
|
|
# relit le bit d'exécution et non core.hooksPath.
|
|
|
|
|
|
status = self.execute.exec_command_live(cmd, source_erplibre=False)
|
|
|
|
|
|
if status:
|
|
|
|
|
|
print(f"{t('Error installing hooks: ')}{status}")
|
|
|
|
|
|
return
|
|
|
|
|
|
print(t("Git hooks installed!"))
|
|
|
|
|
|
for hook in self._GIT_HOOKS:
|
|
|
|
|
|
pose = os.access(os.path.join(absolu, hook), os.X_OK)
|
|
|
|
|
|
marque = t("hook installed") if pose else t("hook not installed")
|
|
|
|
|
|
print(f" {hook:<26} {marque}")
|
|
|
|
|
|
|
2026-09-02 04:01:10 -04:00
|
|
|
|
def _git_set_conflict_style(self):
|
|
|
|
|
|
"""Poser merge.conflictStyle=zdiff3 dans la configuration globale.
|
|
|
|
|
|
|
|
|
|
|
|
zdiff3 ajoute la base commune aux marqueurs de conflit et sort de la
|
|
|
|
|
|
zone contestée les lignes que les deux côtés ont en commun : il reste
|
|
|
|
|
|
moins à arbitrer à la main. Le style demande git 2.35, que toutes les
|
|
|
|
|
|
plateformes supportées dépassent.
|
|
|
|
|
|
|
|
|
|
|
|
La valeur est relue après écriture : « git config » ne rend rien à
|
|
|
|
|
|
l'écriture, et une configuration globale en lecture seule échouerait
|
|
|
|
|
|
sans que le menu le sache.
|
|
|
|
|
|
"""
|
|
|
|
|
|
status = self.execute.exec_command_live(
|
|
|
|
|
|
"git config --global merge.conflictStyle zdiff3",
|
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
if status:
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"❌ {t('Failed to set merge.conflictStyle, see the output above.')}"
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
result = self.execute.exec_command_live(
|
|
|
|
|
|
"git config --global --get merge.conflictStyle",
|
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
quiet=True,
|
|
|
|
|
|
return_status_and_output=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
value = (
|
|
|
|
|
|
" ".join(result[1]).strip() if isinstance(result, tuple) else ""
|
|
|
|
|
|
)
|
|
|
|
|
|
print(f"✅ merge.conflictStyle = {value}")
|
|
|
|
|
|
|
[ADD] script todo : installer Claude Code et opencode, PATH garanti
Ces installateurs posent leur binaire dans un répertoire du HOME que le PATH
d'un shell ne porte pas toujours : sans la ligne d'export, le binaire est là
et la commande reste introuvable. Le répertoire diffère d'un outil à l'autre,
et l'un des deux écrit déjà sa propre ligne — la présence se teste donc sur
le répertoire, pas sur la graphie, et rien n'est ajouté deux fois.
Le PATH d'un processus est figé depuis son démarrage : un nouveau shell est
nécessaire, ce que la sortie dit. Troisième écrivain dans le fichier de
shell, starship compris, d'où l'écriture mise en commun. Vérifié : 31 tests.
--- EN ---
These installers put their binary in a HOME directory that a shell's PATH
does not always carry: without the export line, the binary is there and the
command stays not found. The directory differs from one tool to the other,
and one of the two already writes its own line — presence is therefore tested
on the directory, not on the spelling, and nothing is added twice.
A process PATH is frozen since its start: a new shell is needed, which the
output says. Third writer into the shell file, starship included, hence the
shared write. Checked: 31 tests.
Assisted-by: Claude Opus 5
2026-09-02 04:51:03 -04:00
|
|
|
|
# Le shell -> son fichier de configuration.
|
|
|
|
|
|
_SHELL_RC = {
|
|
|
|
|
|
"bash": "~/.bashrc",
|
|
|
|
|
|
"zsh": "~/.zshrc",
|
|
|
|
|
|
"fish": "~/.config/fish/config.fish",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-03 04:11:45 -04:00
|
|
|
|
# Ces quatre tables vivent dans dev_tools : le déploiement QEMU pose les
|
|
|
|
|
|
# mêmes outils DANS une VM, et deux copies d'une URL amont dérivent dès
|
|
|
|
|
|
# que l'une change. Les noms de classe restent, ils sont l'interface.
|
|
|
|
|
|
_STARSHIP_LINE = dev_tools.STARSHIP_LINE
|
|
|
|
|
|
_STARSHIP_UPSTREAM = dev_tools.STARSHIP_UPSTREAM
|
|
|
|
|
|
_UPSTREAM_TOOLS = dev_tools.AGENTS
|
[ADD] script todo : installer Claude Code et opencode, PATH garanti
Ces installateurs posent leur binaire dans un répertoire du HOME que le PATH
d'un shell ne porte pas toujours : sans la ligne d'export, le binaire est là
et la commande reste introuvable. Le répertoire diffère d'un outil à l'autre,
et l'un des deux écrit déjà sa propre ligne — la présence se teste donc sur
le répertoire, pas sur la graphie, et rien n'est ajouté deux fois.
Le PATH d'un processus est figé depuis son démarrage : un nouveau shell est
nécessaire, ce que la sortie dit. Troisième écrivain dans le fichier de
shell, starship compris, d'où l'écriture mise en commun. Vérifié : 31 tests.
--- EN ---
These installers put their binary in a HOME directory that a shell's PATH
does not always carry: without the export line, the binary is there and the
command stays not found. The directory differs from one tool to the other,
and one of the two already writes its own line — presence is therefore tested
on the directory, not on the spelling, and nothing is added twice.
A process PATH is frozen since its start: a new shell is needed, which the
output says. Third writer into the shell file, starship included, hence the
shared write. Checked: 31 tests.
Assisted-by: Claude Opus 5
2026-09-02 04:51:03 -04:00
|
|
|
|
|
2026-09-02 04:43:25 -04:00
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _shell_name():
|
|
|
|
|
|
"""Le nom du shell de l'utilisateur d'après $SHELL, '' s'il est vide."""
|
|
|
|
|
|
return os.path.basename(os.environ.get("SHELL", "")).strip()
|
|
|
|
|
|
|
[ADD] script todo : installer Claude Code et opencode, PATH garanti
Ces installateurs posent leur binaire dans un répertoire du HOME que le PATH
d'un shell ne porte pas toujours : sans la ligne d'export, le binaire est là
et la commande reste introuvable. Le répertoire diffère d'un outil à l'autre,
et l'un des deux écrit déjà sa propre ligne — la présence se teste donc sur
le répertoire, pas sur la graphie, et rien n'est ajouté deux fois.
Le PATH d'un processus est figé depuis son démarrage : un nouveau shell est
nécessaire, ce que la sortie dit. Troisième écrivain dans le fichier de
shell, starship compris, d'où l'écriture mise en commun. Vérifié : 31 tests.
--- EN ---
These installers put their binary in a HOME directory that a shell's PATH
does not always carry: without the export line, the binary is there and the
command stays not found. The directory differs from one tool to the other,
and one of the two already writes its own line — presence is therefore tested
on the directory, not on the spelling, and nothing is added twice.
A process PATH is frozen since its start: a new shell is needed, which the
output says. Third writer into the shell file, starship included, hence the
shared write. Checked: 31 tests.
Assisted-by: Claude Opus 5
2026-09-02 04:51:03 -04:00
|
|
|
|
def _shell_rc_present(self):
|
|
|
|
|
|
"""Les shells dont le fichier de configuration existe déjà."""
|
|
|
|
|
|
return [
|
|
|
|
|
|
nom
|
|
|
|
|
|
for nom, fichier in self._SHELL_RC.items()
|
|
|
|
|
|
if os.path.exists(os.path.expanduser(fichier))
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
def _shell_rc_target(self):
|
|
|
|
|
|
"""Le shell à modifier. Ne demande que devant un vrai choix.
|
|
|
|
|
|
|
|
|
|
|
|
Aucun fichier de configuration présent : bash, sans question — l'appel
|
|
|
|
|
|
le créera. Un seul présent : celui-là, il n'y a rien à choisir. Deux ou
|
|
|
|
|
|
trois : à l'opérateur de trancher, le sien proposé par défaut.
|
|
|
|
|
|
"""
|
|
|
|
|
|
presents = self._shell_rc_present()
|
|
|
|
|
|
if not presents:
|
|
|
|
|
|
return "bash"
|
|
|
|
|
|
if len(presents) == 1:
|
|
|
|
|
|
return presents[0]
|
|
|
|
|
|
courant = self._shell_name()
|
|
|
|
|
|
defaut = courant if courant in presents else presents[0]
|
|
|
|
|
|
print(f"\n{t('Which shell configuration?')}")
|
|
|
|
|
|
for i, nom in enumerate(presents, 1):
|
|
|
|
|
|
print(f" [{i}] {nom:<5} {self._SHELL_RC[nom]}")
|
|
|
|
|
|
sel = input(
|
|
|
|
|
|
f"{t('Choice (number or name, default:')} {defaut}) : "
|
|
|
|
|
|
).strip()
|
|
|
|
|
|
if not sel:
|
|
|
|
|
|
return defaut
|
|
|
|
|
|
if sel in presents:
|
|
|
|
|
|
return sel
|
|
|
|
|
|
try:
|
|
|
|
|
|
idx = int(sel) - 1
|
|
|
|
|
|
if 0 <= idx < len(presents):
|
|
|
|
|
|
return presents[idx]
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
return defaut
|
|
|
|
|
|
|
|
|
|
|
|
def _shell_rc_append(self, shell, ligne, marqueur):
|
|
|
|
|
|
"""Ajouter la ligne au fichier du shell si le marqueur n'y est pas.
|
|
|
|
|
|
|
|
|
|
|
|
Rend le chemin du fichier quand la ligne est écrite, None quand le
|
|
|
|
|
|
marqueur y était déjà. Le marqueur, et non la ligne entière, parce
|
|
|
|
|
|
qu'une variante écrite à la main ou par un installateur amont compte
|
|
|
|
|
|
autant : ce qui importe est que l'effet soit là, pas la graphie.
|
|
|
|
|
|
"""
|
|
|
|
|
|
chemin = os.path.expanduser(self._SHELL_RC[shell])
|
|
|
|
|
|
contenu = ""
|
|
|
|
|
|
if os.path.exists(chemin):
|
|
|
|
|
|
with open(chemin, encoding="utf-8") as fh:
|
|
|
|
|
|
contenu = fh.read()
|
|
|
|
|
|
if marqueur in contenu:
|
|
|
|
|
|
return None
|
|
|
|
|
|
os.makedirs(os.path.dirname(chemin), exist_ok=True)
|
|
|
|
|
|
with open(chemin, "a", encoding="utf-8") as fh:
|
|
|
|
|
|
# Un fichier qui ne finit pas par un saut de ligne collerait la
|
|
|
|
|
|
# ligne ajoutée à la dernière commande.
|
|
|
|
|
|
if contenu and not contenu.endswith("\n"):
|
|
|
|
|
|
fh.write("\n")
|
|
|
|
|
|
fh.write(f"{ligne}\n")
|
|
|
|
|
|
return chemin
|
|
|
|
|
|
|
|
|
|
|
|
def _shell_path_line(self, shell, repertoire):
|
|
|
|
|
|
"""La ligne qui met un répertoire dans le PATH, selon le shell."""
|
|
|
|
|
|
if shell == "fish":
|
|
|
|
|
|
return f"fish_add_path {repertoire}"
|
|
|
|
|
|
return f'export PATH="{repertoire}:$PATH"'
|
|
|
|
|
|
|
|
|
|
|
|
def _shell_ensure_on_path(self, shell, repertoire):
|
|
|
|
|
|
"""Garantir que le répertoire est dans le PATH du shell choisi.
|
|
|
|
|
|
|
|
|
|
|
|
Ne fait rien si le répertoire y figure déjà, quelle que soit la
|
|
|
|
|
|
graphie — les installateurs amont écrivent souvent la ligne eux-mêmes.
|
|
|
|
|
|
"""
|
|
|
|
|
|
ligne = self._shell_path_line(shell, repertoire)
|
|
|
|
|
|
chemin = self._shell_rc_append(shell, ligne, repertoire)
|
|
|
|
|
|
if chemin is None:
|
|
|
|
|
|
print(f"✅ {t('Already on the PATH: ')}{repertoire}")
|
|
|
|
|
|
return
|
|
|
|
|
|
print(f"✅ {t('PATH line added to: ')}{chemin}")
|
|
|
|
|
|
print(f" {ligne}")
|
|
|
|
|
|
|
2026-09-02 04:43:25 -04:00
|
|
|
|
def _shell_install_starship(self):
|
|
|
|
|
|
"""Poser starship, puis l'accrocher au shell de l'utilisateur.
|
|
|
|
|
|
|
|
|
|
|
|
Deux étapes qui échouent séparément : le binaire, que le gestionnaire
|
|
|
|
|
|
de paquets de la distribution fournit quand il le connaît, et la ligne
|
|
|
|
|
|
d'initialisation dans le fichier de configuration du shell. Sans la
|
|
|
|
|
|
seconde, starship est installé et le prompt ne change pas.
|
|
|
|
|
|
"""
|
|
|
|
|
|
if shutil.which("starship") is None:
|
|
|
|
|
|
self._shell_install_starship_binary()
|
|
|
|
|
|
if shutil.which("starship") is None:
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"❌ {t('starship is not installed, shell left untouched.')}"
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
self._shell_hook_starship()
|
|
|
|
|
|
|
|
|
|
|
|
def _shell_install_starship_binary(self):
|
|
|
|
|
|
"""Poser le binaire : le paquet de la distribution, sinon l'amont.
|
|
|
|
|
|
|
|
|
|
|
|
Un refus de l'opérateur arrête là. Un paquet inconnu ou une
|
|
|
|
|
|
installation en échec passent au recours amont, qui couvre les dépôts
|
|
|
|
|
|
où starship n'est pas empaqueté.
|
|
|
|
|
|
"""
|
|
|
|
|
|
cmd = todo_install.install_command(["starship"])
|
|
|
|
|
|
if cmd:
|
|
|
|
|
|
status = todo_install.ask_and_install(
|
|
|
|
|
|
self.execute,
|
|
|
|
|
|
cmd,
|
|
|
|
|
|
t("Install starship? (y/N): "),
|
|
|
|
|
|
self._is_yes,
|
|
|
|
|
|
)
|
|
|
|
|
|
if status is None:
|
|
|
|
|
|
return
|
|
|
|
|
|
if status == 0 and shutil.which("starship"):
|
|
|
|
|
|
return
|
|
|
|
|
|
print(f" {t('No starship package here, falling back upstream.')}")
|
|
|
|
|
|
todo_install.ask_and_install(
|
|
|
|
|
|
self.execute,
|
|
|
|
|
|
self._STARSHIP_UPSTREAM,
|
|
|
|
|
|
t("Run the upstream installer? (y/N): "),
|
|
|
|
|
|
self._is_yes,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _shell_hook_starship(self):
|
|
|
|
|
|
"""Ajouter la ligne d'initialisation au fichier du shell choisi.
|
|
|
|
|
|
|
|
|
|
|
|
La ligne n'est écrite qu'une fois : « starship init » cherché dans le
|
|
|
|
|
|
fichier couvre les trois shells, dont les lignes diffèrent. L'écriture
|
|
|
|
|
|
ne demande pas de confirmation — le choix du fichier, quand il y en a
|
|
|
|
|
|
un à faire, l'a déjà donnée.
|
|
|
|
|
|
"""
|
[ADD] script todo : installer Claude Code et opencode, PATH garanti
Ces installateurs posent leur binaire dans un répertoire du HOME que le PATH
d'un shell ne porte pas toujours : sans la ligne d'export, le binaire est là
et la commande reste introuvable. Le répertoire diffère d'un outil à l'autre,
et l'un des deux écrit déjà sa propre ligne — la présence se teste donc sur
le répertoire, pas sur la graphie, et rien n'est ajouté deux fois.
Le PATH d'un processus est figé depuis son démarrage : un nouveau shell est
nécessaire, ce que la sortie dit. Troisième écrivain dans le fichier de
shell, starship compris, d'où l'écriture mise en commun. Vérifié : 31 tests.
--- EN ---
These installers put their binary in a HOME directory that a shell's PATH
does not always carry: without the export line, the binary is there and the
command stays not found. The directory differs from one tool to the other,
and one of the two already writes its own line — presence is therefore tested
on the directory, not on the spelling, and nothing is added twice.
A process PATH is frozen since its start: a new shell is needed, which the
output says. Third writer into the shell file, starship included, hence the
shared write. Checked: 31 tests.
Assisted-by: Claude Opus 5
2026-09-02 04:51:03 -04:00
|
|
|
|
shell = self._shell_rc_target()
|
|
|
|
|
|
ligne = self._STARSHIP_LINE[shell]
|
|
|
|
|
|
chemin = self._shell_rc_append(shell, ligne, "starship init")
|
|
|
|
|
|
if chemin is None:
|
|
|
|
|
|
fichier = os.path.expanduser(self._SHELL_RC[shell])
|
|
|
|
|
|
print(f"✅ {t('starship is already hooked into: ')}{fichier}")
|
2026-09-02 04:43:25 -04:00
|
|
|
|
return
|
|
|
|
|
|
print(f"✅ {t('starship hooked into: ')}{chemin}")
|
|
|
|
|
|
print(f" {ligne}")
|
|
|
|
|
|
print(f" {t('Open a new shell to see it.')}")
|
|
|
|
|
|
|
[ADD] script todo : installer Claude Code et opencode, PATH garanti
Ces installateurs posent leur binaire dans un répertoire du HOME que le PATH
d'un shell ne porte pas toujours : sans la ligne d'export, le binaire est là
et la commande reste introuvable. Le répertoire diffère d'un outil à l'autre,
et l'un des deux écrit déjà sa propre ligne — la présence se teste donc sur
le répertoire, pas sur la graphie, et rien n'est ajouté deux fois.
Le PATH d'un processus est figé depuis son démarrage : un nouveau shell est
nécessaire, ce que la sortie dit. Troisième écrivain dans le fichier de
shell, starship compris, d'où l'écriture mise en commun. Vérifié : 31 tests.
--- EN ---
These installers put their binary in a HOME directory that a shell's PATH
does not always carry: without the export line, the binary is there and the
command stays not found. The directory differs from one tool to the other,
and one of the two already writes its own line — presence is therefore tested
on the directory, not on the spelling, and nothing is added twice.
A process PATH is frozen since its start: a new shell is needed, which the
output says. Third writer into the shell file, starship included, hence the
shared write. Checked: 31 tests.
Assisted-by: Claude Opus 5
2026-09-02 04:51:03 -04:00
|
|
|
|
def _shell_install_claude_code(self):
|
|
|
|
|
|
self._shell_install_upstream_tool("claude")
|
|
|
|
|
|
|
|
|
|
|
|
def _shell_install_opencode(self):
|
|
|
|
|
|
self._shell_install_upstream_tool("opencode")
|
|
|
|
|
|
|
|
|
|
|
|
def _shell_install_upstream_tool(self, binaire):
|
|
|
|
|
|
"""Lancer l'installateur amont d'un assistant, puis garantir le PATH.
|
|
|
|
|
|
|
|
|
|
|
|
Ces installateurs posent leur binaire dans un répertoire du HOME que
|
|
|
|
|
|
le PATH d'un shell ne porte pas toujours : sans la ligne d'export, le
|
|
|
|
|
|
binaire est là et la commande reste introuvable. Le PATH du processus
|
|
|
|
|
|
courant, lui, est figé depuis son démarrage — le menu ne verra pas le
|
|
|
|
|
|
binaire avant d'être relancé.
|
|
|
|
|
|
"""
|
|
|
|
|
|
commande, repertoire = self._UPSTREAM_TOOLS[binaire]
|
|
|
|
|
|
status = self.execute.exec_command_live(
|
|
|
|
|
|
commande,
|
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
if status:
|
|
|
|
|
|
print(f"❌ {t('Installation failed, see the output above.')}")
|
|
|
|
|
|
return
|
|
|
|
|
|
self._shell_ensure_on_path(self._shell_rc_target(), repertoire)
|
|
|
|
|
|
pose = os.path.join(os.path.expanduser(repertoire), binaire)
|
|
|
|
|
|
if not os.path.exists(pose):
|
|
|
|
|
|
print(f"⚠ {t('Binary not found at: ')}{pose}")
|
|
|
|
|
|
return
|
|
|
|
|
|
print(f"✅ {binaire} : {pose}")
|
|
|
|
|
|
print(f" {t('Open a new shell to see it.')}")
|
|
|
|
|
|
|
2026-03-09 16:34:43 -04:00
|
|
|
|
def prompt_execute_git_local_server(self):
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(f"🤖 {t('Manage local git repository server!')}")
|
2026-03-10 03:45:11 -04:00
|
|
|
|
choices = [
|
2026-03-13 15:04:14 -04:00
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
|
"Deploy a local git server (~/.git-server)"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
|
"Deploy a production git server (/srv/git, root required)"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
2026-03-09 16:34:43 -04:00
|
|
|
|
]
|
2026-03-10 03:45:11 -04:00
|
|
|
|
help_info = self.fill_help_info(choices)
|
2026-03-09 16:34:43 -04:00
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
|
print()
|
|
|
|
|
|
if status == "0":
|
|
|
|
|
|
return False
|
|
|
|
|
|
elif status == "1":
|
|
|
|
|
|
self._prompt_git_server_actions(production_ready=False)
|
|
|
|
|
|
elif status == "2":
|
|
|
|
|
|
self._prompt_git_server_actions(production_ready=True)
|
|
|
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(t("Command not found !"))
|
2026-03-09 16:34:43 -04:00
|
|
|
|
|
|
|
|
|
|
def _prompt_git_server_actions(self, production_ready=False):
|
|
|
|
|
|
mode = (
|
2026-03-12 05:31:45 -04:00
|
|
|
|
t("Production mode (/srv/git, root required)")
|
2026-03-09 16:34:43 -04:00
|
|
|
|
if production_ready
|
2026-03-12 05:31:45 -04:00
|
|
|
|
else t("Local mode (~/.git-server)")
|
2026-03-09 16:34:43 -04:00
|
|
|
|
)
|
|
|
|
|
|
print(f"🤖 {mode}")
|
2026-03-10 03:45:11 -04:00
|
|
|
|
choices = [
|
2026-03-13 15:04:14 -04:00
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
|
"Run all (init + remote + push + serve)"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
2026-03-12 05:31:45 -04:00
|
|
|
|
{"prompt_description": t("Init - Create bare repos")},
|
|
|
|
|
|
{"prompt_description": t("Remote - Add local remotes")},
|
|
|
|
|
|
{"prompt_description": t("Push - Push to local server")},
|
|
|
|
|
|
{"prompt_description": t("Serve - Start git daemon")},
|
2026-03-09 16:34:43 -04:00
|
|
|
|
]
|
2026-03-10 03:45:11 -04:00
|
|
|
|
help_info = self.fill_help_info(choices)
|
2026-03-09 16:34:43 -04:00
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
|
print()
|
|
|
|
|
|
if status == "0":
|
|
|
|
|
|
return False
|
|
|
|
|
|
elif status == "1":
|
|
|
|
|
|
self._deploy_git_server(
|
|
|
|
|
|
production_ready=production_ready,
|
|
|
|
|
|
action="all",
|
|
|
|
|
|
)
|
|
|
|
|
|
elif status == "2":
|
|
|
|
|
|
self._deploy_git_server(
|
|
|
|
|
|
production_ready=production_ready,
|
|
|
|
|
|
action="init",
|
|
|
|
|
|
)
|
|
|
|
|
|
elif status == "3":
|
|
|
|
|
|
self._deploy_git_server(
|
|
|
|
|
|
production_ready=production_ready,
|
|
|
|
|
|
action="remote",
|
|
|
|
|
|
)
|
|
|
|
|
|
elif status == "4":
|
|
|
|
|
|
self._deploy_git_server(
|
|
|
|
|
|
production_ready=production_ready,
|
|
|
|
|
|
action="push",
|
|
|
|
|
|
)
|
|
|
|
|
|
elif status == "5":
|
|
|
|
|
|
self._deploy_git_server(
|
|
|
|
|
|
production_ready=production_ready,
|
|
|
|
|
|
action="serve",
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(t("Command not found !"))
|
2026-03-09 16:34:43 -04:00
|
|
|
|
|
|
|
|
|
|
def _deploy_git_server(self, production_ready=False, action="all"):
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(t("Starting git server deployment..."))
|
[ADD] déploiement : un proxy SOCKS par SSH, et son mode d'emploi
« -L » relaie UN service ; « -D » ouvre un relais SOCKS, par lequel le
navigateur atteint n'importe quelle destination depuis la machine distante —
une interface qui n'écoute que sur sa boucle locale, un hôte de son réseau sans
route depuis ici. Le port par défaut est 1080, modifiable, et contrôlé libre
avant d'ouvrir. La saisie de l'adresse est celle de sshfs, désormais partagée :
l'alias de ~/.ssh/config part tel quel à ssh, faute de quoi son ProxyJump se
perdrait et une VM imbriquée deviendrait injoignable. Le réglage de Firefox
s'affiche AVANT le lancement, la commande ne rendant la main qu'au Ctrl+C.
Vérifié : 9 tests neufs, et la numérotation du menu que deux autres gardent.
--- EN ---
"-L" relays ONE service; "-D" opens a SOCKS relay, through which the browser
reaches any destination from the remote machine — an interface listening only
on its loopback, a host of its network with no route from here. The default
port is 1080, changeable, and checked free before opening. The address prompt
is sshfs's, now shared: the ~/.ssh/config alias goes to ssh as is, failing
which its ProxyJump would be lost and a nested VM become unreachable. The
Firefox settings print BEFORE the launch, the command only returning on
Ctrl+C.
Checked: 9 new tests, and the menu numbering two others guard.
Assisted-by: Claude Opus 5
2026-09-24 13:32:28 -04:00
|
|
|
|
cmd = f"python3 ./script/git/git_local_server.py -v --action {action}"
|
2026-03-09 16:34:43 -04:00
|
|
|
|
if production_ready:
|
|
|
|
|
|
cmd += " --production-ready"
|
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
|
cmd,
|
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-03-10 02:05:53 -04:00
|
|
|
|
def prompt_execute_gpt_code(self):
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(f"🤖 {t('AI assistant tools for development!')}")
|
2026-03-10 03:45:11 -04:00
|
|
|
|
choices = [
|
2026-03-12 05:31:45 -04:00
|
|
|
|
{"prompt_description": t("Configure Claude Code configurations")},
|
2026-03-13 15:04:14 -04:00
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
|
"Add an automation with Claude in todo.py"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
|
"RTK - CLI proxy to reduce LLM token consumption"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
[ADD] todo : afficher le contexte fourni à Claude
Le contexte d'un assistant est éparpillé sur six sources — instructions,
règles, skills, commandes déployées, hooks git, mémoire — et rien ne disait
laquelle manquait ni laquelle avait dérivé.
L'écran les relève. Chaque commande déployée est comparée à son gabarit du
dépôt en ignorant les lignes d'identité git, que le déploiement substitue :
une copie périmée se voit, là où une égalité stricte les déclarerait toutes
périmées. Les termes de la liste privée sont COMPTÉS, jamais affichés.
--- EN ---
An assistant's context is scattered over six sources — instructions, rules,
skills, deployed commands, git hooks, memory — and nothing said which one was
missing nor which one had drifted.
The screen reports them. Each deployed command is compared to its repository
template, ignoring the git identity lines the deployment substitutes: a stale
copy shows up, where a strict equality would declare them all stale. The
private list's terms are COUNTED, never displayed.
Assisted-by: Claude Opus 5
2026-08-30 02:49:40 -04:00
|
|
|
|
{"prompt_description": t("Show the context given to Claude")},
|
[ADD] script todo : gérer les plugins Claude Code et leur liste ERPLibre
Rien ne posait un plugin depuis TODO ; la CLI seule le faisait, hors du
menu. L'installation passe par « -y » : la sortie de TODO est un tuyau,
pas un terminal, et la CLI refuse sans lui toute installation qui
exécute une commande déclarée par un marketplace. La liste préférée
s'affiche donc AVANT la confirmation, seule occasion de la lire ; ses
quatre plugins travaillent sur le poste, sans service tiers ni compte.
La recherche lit les manifestes sur le disque, donc hors ligne.
Vérifié : 8 tests neufs, dont la frontière de mot qui sépare deux noms
dont l'un contient l'autre — une recherche naïve en rate trois.
--- EN ---
Nothing installed a plugin from TODO; the CLI alone did, outside the
menu. Installing goes through « -y »: TODO's output is a pipe, not a
terminal, and without it the CLI refuses any install that runs a
command declared by a marketplace. The preferred list is therefore
shown BEFORE the confirmation, the only chance to read it; its four
plugins run on the workstation, with no third party and no account.
Search reads the manifests from disk, hence offline.
Checked: 8 new tests, among them the word boundary separating two names
where one contains the other — a naive search misses three of them.
Assisted-by: Claude Opus 5
2026-09-02 06:35:05 -04:00
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
|
"Claude Code plugins - marketplaces and ERPLibre list"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
[ADD] assistant : serveur LLM reconnu, catalogue gpt, sessions locales
L'entrée « Question IA » envoyait toute question à une seule API distante, sur
un modèle figé et sans historique : aucun serveur local n'était atteignable,
et rien ne disait où partait le texte.
Deux règles gouvernent ce qui la remplace. Un port ne dit jamais qui répond —
un seul en héberge jusqu'à trois, et une famille réémet l'API d'une autre en
entier — donc l'identité se lit dans le CORPS, par une échelle ordonnée. Et
une adresse ne devient jamais du texte d'invite : le détecteur du dépôt ne
voit pas les noms d'hôte, d'où une poignée opaque à sa place.
Vérifié : 284 tests, et contre un serveur du réseau — reconnaissance,
capacités lues, conversation multi-tours, triage juste.
--- EN ---
The "AI question" entry sent every question to a single remote API, on a
frozen model and with no history: no local server was reachable, and nothing
said where the text was going.
Two rules govern what replaces it. A port never says who answers — one hosts
up to three products, and one family re-serves another's native API in full —
so identity is read from the BODY, by an ordered ladder. And an address never
becomes prompt text: the repository's detector does not see host names, hence
an opaque handle in its place.
Checked: 284 tests, and against a server on the network — recognition,
capabilities read, a multi-turn conversation, a correct triage.
Assisted-by: Claude Opus 5
2026-09-09 06:41:56 -04:00
|
|
|
|
{"prompt_description": t("Claude Code - local sessions")},
|
2026-03-10 02:05:53 -04:00
|
|
|
|
]
|
2026-03-10 03:45:11 -04:00
|
|
|
|
help_info = self.fill_help_info(choices)
|
2026-03-10 02:05:53 -04:00
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
|
print()
|
|
|
|
|
|
if status == "0":
|
|
|
|
|
|
return False
|
|
|
|
|
|
elif status == "1":
|
2026-03-12 04:37:19 -04:00
|
|
|
|
self._prompt_claude_configs()
|
2026-03-12 02:51:01 -04:00
|
|
|
|
elif status == "2":
|
2026-03-12 04:04:52 -04:00
|
|
|
|
self._claude_add_automation()
|
|
|
|
|
|
elif status == "3":
|
2026-03-12 02:51:01 -04:00
|
|
|
|
self.prompt_execute_rtk()
|
[ADD] todo : afficher le contexte fourni à Claude
Le contexte d'un assistant est éparpillé sur six sources — instructions,
règles, skills, commandes déployées, hooks git, mémoire — et rien ne disait
laquelle manquait ni laquelle avait dérivé.
L'écran les relève. Chaque commande déployée est comparée à son gabarit du
dépôt en ignorant les lignes d'identité git, que le déploiement substitue :
une copie périmée se voit, là où une égalité stricte les déclarerait toutes
périmées. Les termes de la liste privée sont COMPTÉS, jamais affichés.
--- EN ---
An assistant's context is scattered over six sources — instructions, rules,
skills, deployed commands, git hooks, memory — and nothing said which one was
missing nor which one had drifted.
The screen reports them. Each deployed command is compared to its repository
template, ignoring the git identity lines the deployment substitutes: a stale
copy shows up, where a strict equality would declare them all stale. The
private list's terms are COUNTED, never displayed.
Assisted-by: Claude Opus 5
2026-08-30 02:49:40 -04:00
|
|
|
|
elif status == "4":
|
|
|
|
|
|
self._show_claude_context()
|
[ADD] script todo : gérer les plugins Claude Code et leur liste ERPLibre
Rien ne posait un plugin depuis TODO ; la CLI seule le faisait, hors du
menu. L'installation passe par « -y » : la sortie de TODO est un tuyau,
pas un terminal, et la CLI refuse sans lui toute installation qui
exécute une commande déclarée par un marketplace. La liste préférée
s'affiche donc AVANT la confirmation, seule occasion de la lire ; ses
quatre plugins travaillent sur le poste, sans service tiers ni compte.
La recherche lit les manifestes sur le disque, donc hors ligne.
Vérifié : 8 tests neufs, dont la frontière de mot qui sépare deux noms
dont l'un contient l'autre — une recherche naïve en rate trois.
--- EN ---
Nothing installed a plugin from TODO; the CLI alone did, outside the
menu. Installing goes through « -y »: TODO's output is a pipe, not a
terminal, and without it the CLI refuses any install that runs a
command declared by a marketplace. The preferred list is therefore
shown BEFORE the confirmation, the only chance to read it; its four
plugins run on the workstation, with no third party and no account.
Search reads the manifests from disk, hence offline.
Checked: 8 new tests, among them the word boundary separating two names
where one contains the other — a naive search misses three of them.
Assisted-by: Claude Opus 5
2026-09-02 06:35:05 -04:00
|
|
|
|
elif status == "5":
|
|
|
|
|
|
self.prompt_execute_claude_plugins()
|
[ADD] assistant : serveur LLM reconnu, catalogue gpt, sessions locales
L'entrée « Question IA » envoyait toute question à une seule API distante, sur
un modèle figé et sans historique : aucun serveur local n'était atteignable,
et rien ne disait où partait le texte.
Deux règles gouvernent ce qui la remplace. Un port ne dit jamais qui répond —
un seul en héberge jusqu'à trois, et une famille réémet l'API d'une autre en
entier — donc l'identité se lit dans le CORPS, par une échelle ordonnée. Et
une adresse ne devient jamais du texte d'invite : le détecteur du dépôt ne
voit pas les noms d'hôte, d'où une poignée opaque à sa place.
Vérifié : 284 tests, et contre un serveur du réseau — reconnaissance,
capacités lues, conversation multi-tours, triage juste.
--- EN ---
The "AI question" entry sent every question to a single remote API, on a
frozen model and with no history: no local server was reachable, and nothing
said where the text was going.
Two rules govern what replaces it. A port never says who answers — one hosts
up to three products, and one family re-serves another's native API in full —
so identity is read from the BODY, by an ordered ladder. And an address never
becomes prompt text: the repository's detector does not see host names, hence
an opaque handle in its place.
Checked: 284 tests, and against a server on the network — recognition,
capabilities read, a multi-turn conversation, a correct triage.
Assisted-by: Claude Opus 5
2026-09-09 06:41:56 -04:00
|
|
|
|
elif status == "6":
|
|
|
|
|
|
self.prompt_claude_sessions()
|
2026-03-10 02:05:53 -04:00
|
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(t("Command not found !"))
|
2026-03-10 02:05:53 -04:00
|
|
|
|
|
2026-03-12 04:37:19 -04:00
|
|
|
|
def _prompt_claude_configs(self):
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(f"🤖 {t('Deploy Claude Code commands!')}")
|
2026-03-12 04:37:19 -04:00
|
|
|
|
choices = [
|
2026-03-12 05:31:45 -04:00
|
|
|
|
{"prompt_description": t("Commit - OCA/Odoo commit command")},
|
[ADD] script todo : déployer /git_prepare_merge depuis le menu Claude
Une fusion apporte plusieurs commits d'un coup, et rien ne guidait les
deux écrits qu'elle demande : l'entrée de changelog et le message de
merge. La commande déployée impose la source — CHANGELOG.base.md, les
fichiers générés étant perdus au prochain doc_markdown — et le corps
bilingue avec son trailer, que le garde-fou ne verra jamais, commit-msg
ignorant tout message ouvrant sur « Merge ». Elle prépare, elle ne
fusionne pas : le message part dans tasks/, non versionné.
Vérifié : 45 tests du menu, et un déploiement dans un HOME jetable que
l'écran de contexte relit « à jour » face à son gabarit.
--- EN ---
A merge lands several commits at once, and nothing guided the two pieces
of writing it needs: the changelog entry and the merge message. The
deployed command imposes the source — CHANGELOG.base.md, the generated
files being lost at the next doc_markdown — and the bilingual body with
its trailer, which the guard rail never sees, commit-msg skipping any
message opening on « Merge ». It prepares, it does not merge: the
message goes to tasks/, which is not versioned.
Checked: 45 menu tests, and a deployment into a throwaway HOME that the
context screen reads back as up to date against its template.
Assisted-by: Claude Opus 5
2026-09-02 06:15:43 -04:00
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
|
"Git prepare merge - Git merge preparation command"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
2026-03-12 04:37:19 -04:00
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
[ADD] script todo : /todo_plan_max, planifier avant d'ajouter une entrée
Ajouter une entrée commence par des décisions qu'aucun gabarit
n'imposait : quel menu parent, motif A ou B, quoi faire en cas d'échec,
ce qui est détruit, ce qui touche à une donnée client. La commande les
demande avant d'écrire, planifie avec superpowers quand le plugin est
là, et pose sa spécification dans tasks/, non versionné. Son frontmatter
fixe l'effort à max, seule valeur de l'énumération ; ultracode est un
épinglage de session que l'utilisateur tape, non un réglage de gabarit.
Vérifié : les deux gabarits déployés dans un HOME jetable, relus « à
jour » par l'écran de contexte.
--- EN ---
Adding an entry starts with decisions no template forced: which parent
menu, pattern A or B, what to do on failure, what gets destroyed, what
touches customer data. The command asks them before writing, plans with
superpowers when the plugin is there, and lays its specification in
tasks/, which is not versioned. Its frontmatter sets effort to max, the
only value in the enumeration; ultracode is a session pin the user
types, not a template setting.
Checked: both templates deployed into a throwaway HOME, read back as up
to date by the context screen.
Assisted-by: Claude Opus 5
2026-09-02 06:35:57 -04:00
|
|
|
|
"Todo Add Command + Plan Max - Plan and add a todo.py"
|
|
|
|
|
|
" command"
|
2026-03-12 04:37:19 -04:00
|
|
|
|
)
|
|
|
|
|
|
},
|
[ADD] script todo : /todo_generate_code, effort high et règles du dépôt
Les règles à appliquer avant de coder sont éparpillées entre les configs,
les hooks et les modules maison, et la documentation en contredit
plusieurs : autopep8 est recommandé alors que le script sort en 1, aucun
script oca-* n'est installé, la racine n'a ni .pre-commit-config.yaml ni
.pylintrc, et flake8 comme pylint-odoo ne vivent que dans le venv Odoo,
que rien ne lance. Le gabarit énonce ce que l'outillage impose, à effort
high, l'épinglage ultracode restant celui de l'utilisateur.
Vérifié : 146 règles relevées, 142 confirmées contre leur citation, 4
retirées. Deux gardes lient chaque entrée du menu à un gabarit présent
qui déclare le bon nom.
--- EN ---
The rules to apply before coding are scattered across the configs, the
hooks and the in-house modules, and the documentation contradicts several
of them: autopep8 is recommended although the script exits 1, no oca-*
script is installed, the root carries neither .pre-commit-config.yaml nor
.pylintrc, and flake8 as well as pylint-odoo live only in the Odoo venv,
which nothing invokes. The template states what the tooling enforces, at
high effort, the ultracode pin remaining the user's own.
Checked: 146 rules surveyed, 142 confirmed against their citation, 4
dropped. Two guards tie each menu entry to a template that exists and
declares the right name.
Assisted-by: Claude Opus 5
2026-09-02 07:16:23 -04:00
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
[ADD] déploiement : un proxy SOCKS par SSH, et son mode d'emploi
« -L » relaie UN service ; « -D » ouvre un relais SOCKS, par lequel le
navigateur atteint n'importe quelle destination depuis la machine distante —
une interface qui n'écoute que sur sa boucle locale, un hôte de son réseau sans
route depuis ici. Le port par défaut est 1080, modifiable, et contrôlé libre
avant d'ouvrir. La saisie de l'adresse est celle de sshfs, désormais partagée :
l'alias de ~/.ssh/config part tel quel à ssh, faute de quoi son ProxyJump se
perdrait et une VM imbriquée deviendrait injoignable. Le réglage de Firefox
s'affiche AVANT le lancement, la commande ne rendant la main qu'au Ctrl+C.
Vérifié : 9 tests neufs, et la numérotation du menu que deux autres gardent.
--- EN ---
"-L" relays ONE service; "-D" opens a SOCKS relay, through which the browser
reaches any destination from the remote machine — an interface listening only
on its loopback, a host of its network with no route from here. The default
port is 1080, changeable, and checked free before opening. The address prompt
is sshfs's, now shared: the ~/.ssh/config alias goes to ssh as is, failing
which its ProxyJump would be lost and a nested VM become unreachable. The
Firefox settings print BEFORE the launch, the command only returning on
Ctrl+C.
Checked: 9 new tests, and the menu numbering two others guard.
Assisted-by: Claude Opus 5
2026-09-24 13:32:28 -04:00
|
|
|
|
"Todo Generate Code - Code by the OCA rules at high effort"
|
[ADD] script todo : /todo_generate_code, effort high et règles du dépôt
Les règles à appliquer avant de coder sont éparpillées entre les configs,
les hooks et les modules maison, et la documentation en contredit
plusieurs : autopep8 est recommandé alors que le script sort en 1, aucun
script oca-* n'est installé, la racine n'a ni .pre-commit-config.yaml ni
.pylintrc, et flake8 comme pylint-odoo ne vivent que dans le venv Odoo,
que rien ne lance. Le gabarit énonce ce que l'outillage impose, à effort
high, l'épinglage ultracode restant celui de l'utilisateur.
Vérifié : 146 règles relevées, 142 confirmées contre leur citation, 4
retirées. Deux gardes lient chaque entrée du menu à un gabarit présent
qui déclare le bon nom.
--- EN ---
The rules to apply before coding are scattered across the configs, the
hooks and the in-house modules, and the documentation contradicts several
of them: autopep8 is recommended although the script exits 1, no oca-*
script is installed, the root carries neither .pre-commit-config.yaml nor
.pylintrc, and flake8 as well as pylint-odoo live only in the Odoo venv,
which nothing invokes. The template states what the tooling enforces, at
high effort, the ultracode pin remaining the user's own.
Checked: 146 rules surveyed, 142 confirmed against their citation, 4
dropped. Two guards tie each menu entry to a template that exists and
declares the right name.
Assisted-by: Claude Opus 5
2026-09-02 07:16:23 -04:00
|
|
|
|
)
|
|
|
|
|
|
},
|
2026-03-13 15:04:14 -04:00
|
|
|
|
{"prompt_description": t("Show installed custom commands")},
|
2026-03-12 04:37:19 -04:00
|
|
|
|
]
|
|
|
|
|
|
help_info = self.fill_help_info(choices)
|
2026-03-10 02:05:53 -04:00
|
|
|
|
|
2026-03-12 04:37:19 -04:00
|
|
|
|
while True:
|
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
|
print()
|
|
|
|
|
|
if status == "0":
|
|
|
|
|
|
return False
|
|
|
|
|
|
elif status == "1":
|
|
|
|
|
|
self._setup_claude_command(
|
|
|
|
|
|
"commit",
|
|
|
|
|
|
"template_claude_commands_commit.md",
|
|
|
|
|
|
personalize=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
elif status == "2":
|
[ADD] script todo : déployer /git_prepare_merge depuis le menu Claude
Une fusion apporte plusieurs commits d'un coup, et rien ne guidait les
deux écrits qu'elle demande : l'entrée de changelog et le message de
merge. La commande déployée impose la source — CHANGELOG.base.md, les
fichiers générés étant perdus au prochain doc_markdown — et le corps
bilingue avec son trailer, que le garde-fou ne verra jamais, commit-msg
ignorant tout message ouvrant sur « Merge ». Elle prépare, elle ne
fusionne pas : le message part dans tasks/, non versionné.
Vérifié : 45 tests du menu, et un déploiement dans un HOME jetable que
l'écran de contexte relit « à jour » face à son gabarit.
--- EN ---
A merge lands several commits at once, and nothing guided the two pieces
of writing it needs: the changelog entry and the merge message. The
deployed command imposes the source — CHANGELOG.base.md, the generated
files being lost at the next doc_markdown — and the bilingual body with
its trailer, which the guard rail never sees, commit-msg skipping any
message opening on « Merge ». It prepares, it does not merge: the
message goes to tasks/, which is not versioned.
Checked: 45 menu tests, and a deployment into a throwaway HOME that the
context screen reads back as up to date against its template.
Assisted-by: Claude Opus 5
2026-09-02 06:15:43 -04:00
|
|
|
|
self._setup_claude_command(
|
|
|
|
|
|
"git_prepare_merge",
|
|
|
|
|
|
"template_claude_commands_git_prepare_merge.md",
|
|
|
|
|
|
)
|
|
|
|
|
|
elif status == "3":
|
[ADD] script todo : /todo_plan_max, planifier avant d'ajouter une entrée
Ajouter une entrée commence par des décisions qu'aucun gabarit
n'imposait : quel menu parent, motif A ou B, quoi faire en cas d'échec,
ce qui est détruit, ce qui touche à une donnée client. La commande les
demande avant d'écrire, planifie avec superpowers quand le plugin est
là, et pose sa spécification dans tasks/, non versionné. Son frontmatter
fixe l'effort à max, seule valeur de l'énumération ; ultracode est un
épinglage de session que l'utilisateur tape, non un réglage de gabarit.
Vérifié : les deux gabarits déployés dans un HOME jetable, relus « à
jour » par l'écran de contexte.
--- EN ---
Adding an entry starts with decisions no template forced: which parent
menu, pattern A or B, what to do on failure, what gets destroyed, what
touches customer data. The command asks them before writing, plans with
superpowers when the plugin is there, and lays its specification in
tasks/, which is not versioned. Its frontmatter sets effort to max, the
only value in the enumeration; ultracode is a session pin the user
types, not a template setting.
Checked: both templates deployed into a throwaway HOME, read back as up
to date by the context screen.
Assisted-by: Claude Opus 5
2026-09-02 06:35:57 -04:00
|
|
|
|
# Les deux gabarits vont ensemble : /todo_plan_max produit la
|
|
|
|
|
|
# spécification que /todo_add_command implémente, et l'un sans
|
|
|
|
|
|
# l'autre laisse la moitié de la chaîne.
|
|
|
|
|
|
self._setup_claude_command(
|
|
|
|
|
|
"todo_plan_max",
|
|
|
|
|
|
"template_claude_commands_todo_plan_max.md",
|
|
|
|
|
|
)
|
2026-03-12 04:37:19 -04:00
|
|
|
|
self._setup_claude_command(
|
|
|
|
|
|
"todo_add_command",
|
|
|
|
|
|
"template_claude_commands_todo_add_command.md",
|
|
|
|
|
|
)
|
[ADD] script todo : déployer /git_prepare_merge depuis le menu Claude
Une fusion apporte plusieurs commits d'un coup, et rien ne guidait les
deux écrits qu'elle demande : l'entrée de changelog et le message de
merge. La commande déployée impose la source — CHANGELOG.base.md, les
fichiers générés étant perdus au prochain doc_markdown — et le corps
bilingue avec son trailer, que le garde-fou ne verra jamais, commit-msg
ignorant tout message ouvrant sur « Merge ». Elle prépare, elle ne
fusionne pas : le message part dans tasks/, non versionné.
Vérifié : 45 tests du menu, et un déploiement dans un HOME jetable que
l'écran de contexte relit « à jour » face à son gabarit.
--- EN ---
A merge lands several commits at once, and nothing guided the two pieces
of writing it needs: the changelog entry and the merge message. The
deployed command imposes the source — CHANGELOG.base.md, the generated
files being lost at the next doc_markdown — and the bilingual body with
its trailer, which the guard rail never sees, commit-msg skipping any
message opening on « Merge ». It prepares, it does not merge: the
message goes to tasks/, which is not versioned.
Checked: 45 menu tests, and a deployment into a throwaway HOME that the
context screen reads back as up to date against its template.
Assisted-by: Claude Opus 5
2026-09-02 06:15:43 -04:00
|
|
|
|
elif status == "4":
|
[ADD] script todo : /todo_generate_code, effort high et règles du dépôt
Les règles à appliquer avant de coder sont éparpillées entre les configs,
les hooks et les modules maison, et la documentation en contredit
plusieurs : autopep8 est recommandé alors que le script sort en 1, aucun
script oca-* n'est installé, la racine n'a ni .pre-commit-config.yaml ni
.pylintrc, et flake8 comme pylint-odoo ne vivent que dans le venv Odoo,
que rien ne lance. Le gabarit énonce ce que l'outillage impose, à effort
high, l'épinglage ultracode restant celui de l'utilisateur.
Vérifié : 146 règles relevées, 142 confirmées contre leur citation, 4
retirées. Deux gardes lient chaque entrée du menu à un gabarit présent
qui déclare le bon nom.
--- EN ---
The rules to apply before coding are scattered across the configs, the
hooks and the in-house modules, and the documentation contradicts several
of them: autopep8 is recommended although the script exits 1, no oca-*
script is installed, the root carries neither .pre-commit-config.yaml nor
.pylintrc, and flake8 as well as pylint-odoo live only in the Odoo venv,
which nothing invokes. The template states what the tooling enforces, at
high effort, the ultracode pin remaining the user's own.
Checked: 146 rules surveyed, 142 confirmed against their citation, 4
dropped. Two guards tie each menu entry to a template that exists and
declares the right name.
Assisted-by: Claude Opus 5
2026-09-02 07:16:23 -04:00
|
|
|
|
self._setup_claude_command(
|
|
|
|
|
|
"todo_generate_code",
|
|
|
|
|
|
"template_claude_commands_todo_generate_code.md",
|
|
|
|
|
|
)
|
|
|
|
|
|
elif status == "5":
|
2026-03-12 04:37:19 -04:00
|
|
|
|
self._list_claude_commands()
|
|
|
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(t("Command not found !"))
|
2026-03-12 04:37:19 -04:00
|
|
|
|
|
[IMP] todo claude configs: list missing and outdated commands, redeploy
The command list showed only what sat in ~/.claude/commands, with a date:
a command never installed stayed invisible, and a copy older than its
template in conf/ looked the same as a current one. Each ERPLibre template is
now compared with its copy, ignoring the git identity line: not installed,
up to date, or to redeploy with its +/- line count; other commands are
marked not from ERPLibre. On y/N it shows the diff, then redeploys the
outdated copies, keeping the name and e-mail /commit carried.
Checked: 7 new tests on a temporary directory; test_todo, 81 tests, passes.
--- FR ---
[IMP] todo configs claude : commandes absentes, périmées, redéploiement
La liste des commandes ne montrait que le contenu de ~/.claude/commands,
avec une date : une commande jamais installée restait invisible, et une copie
plus vieille que son gabarit de conf/ ressemblait à une copie à jour. Chaque
gabarit d'ERPLibre est désormais comparé à sa copie, hors ligne d'identité
git : non installée, à jour, ou à redéployer avec son compte +/- ; les autres
commandes sont marquées hors ERPLibre. Sur o/N elle montre le diff, puis
redéploie les copies périmées en gardant le nom et le courriel de /commit.
Vérifié : 7 nouveaux tests sur un dossier temporaire ; test_todo, 81 tests.
Assisted-by: Claude Opus 5.5
2026-09-25 01:38:25 -04:00
|
|
|
|
# Les commandes `/…` qu'ERPLibre déploie, et le gabarit de `conf/` dont
|
|
|
|
|
|
# chacune est la copie. La liste et l'écran de contexte en dérivent l'état
|
|
|
|
|
|
# de chaque copie ; le menu nomme les siens en littéraux, que les tests
|
|
|
|
|
|
# relient à `conf/`.
|
|
|
|
|
|
_CLAUDE_COMMAND_TEMPLATES = {
|
|
|
|
|
|
"commit": "template_claude_commands_commit.md",
|
|
|
|
|
|
"git_prepare_merge": "template_claude_commands_git_prepare_merge.md",
|
|
|
|
|
|
"todo_add_command": "template_claude_commands_todo_add_command.md",
|
|
|
|
|
|
"todo_generate_code": "template_claude_commands_todo_generate_code.md",
|
|
|
|
|
|
"todo_plan_max": "template_claude_commands_todo_plan_max.md",
|
|
|
|
|
|
}
|
|
|
|
|
|
_CLAUDE_COMMANDS_DIR = "~/.claude/commands"
|
|
|
|
|
|
|
|
|
|
|
|
# La ligne où le déploiement de /commit substitue le nom et le courriel.
|
|
|
|
|
|
_IDENTITE_GIT = re.compile(
|
|
|
|
|
|
r'user\.name="(?P<nom>[^"]*)" -c user\.email="(?P<courriel>[^"]*)"'
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _claude_template_path(self, gabarit):
|
|
|
|
|
|
"""Le chemin d'un gabarit de commande dans `conf/`."""
|
|
|
|
|
|
return os.path.join(self._claude_context_root(), "conf", gabarit)
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _lignes_stables(chemin):
|
|
|
|
|
|
"""Les lignes d'un fichier, hors celles qui portent l'identité git.
|
|
|
|
|
|
|
|
|
|
|
|
Le déploiement de /commit y substitue le nom et le courriel : les
|
|
|
|
|
|
comparer déclarerait périmée toute copie personnalisée.
|
|
|
|
|
|
"""
|
|
|
|
|
|
with open(chemin, encoding="utf-8", errors="replace") as fh:
|
|
|
|
|
|
return [x for x in fh if "user.name=" not in x]
|
|
|
|
|
|
|
|
|
|
|
|
def _claude_command_diff(self, deployed, template):
|
|
|
|
|
|
"""Le diff unifié de la copie déployée vers le gabarit.
|
|
|
|
|
|
|
|
|
|
|
|
Rend une liste de lignes, vide quand les deux concordent hors
|
|
|
|
|
|
identité git. Les lignes « + » sont ce qu'un redéploiement apporte,
|
|
|
|
|
|
les lignes « - » ce qu'il retire.
|
|
|
|
|
|
"""
|
|
|
|
|
|
return list(
|
|
|
|
|
|
difflib.unified_diff(
|
|
|
|
|
|
self._lignes_stables(deployed),
|
|
|
|
|
|
self._lignes_stables(template),
|
|
|
|
|
|
fromfile=t("installed copy"),
|
|
|
|
|
|
tofile=t("template"),
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _compte_diff(diff):
|
|
|
|
|
|
"""(ajoutées, retirées) d'un diff unifié, en-têtes exclus."""
|
|
|
|
|
|
plus = sum(
|
|
|
|
|
|
1 for x in diff if x.startswith("+") and not x.startswith("+++")
|
|
|
|
|
|
)
|
|
|
|
|
|
moins = sum(
|
|
|
|
|
|
1 for x in diff if x.startswith("-") and not x.startswith("---")
|
|
|
|
|
|
)
|
|
|
|
|
|
return plus, moins
|
|
|
|
|
|
|
|
|
|
|
|
def _redeploy_claude_command(self, command_name, template_filename):
|
|
|
|
|
|
"""Réécrit une copie déployée depuis son gabarit, SANS question.
|
|
|
|
|
|
|
|
|
|
|
|
L'identité git que la copie portait est reportée dans la nouvelle :
|
|
|
|
|
|
une mise à jour ne redemande ni le nom ni le courriel. Rend True si
|
|
|
|
|
|
le fichier est écrit.
|
|
|
|
|
|
"""
|
|
|
|
|
|
dest_file = os.path.join(
|
|
|
|
|
|
os.path.expanduser(self._CLAUDE_COMMANDS_DIR),
|
|
|
|
|
|
f"{command_name}.md",
|
|
|
|
|
|
)
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(
|
|
|
|
|
|
self._claude_template_path(template_filename),
|
|
|
|
|
|
encoding="utf-8",
|
|
|
|
|
|
) as fh:
|
|
|
|
|
|
content = fh.read()
|
|
|
|
|
|
with open(dest_file, encoding="utf-8", errors="replace") as fh:
|
|
|
|
|
|
identite = self._IDENTITE_GIT.search(fh.read())
|
|
|
|
|
|
if identite:
|
|
|
|
|
|
content = content.replace(
|
|
|
|
|
|
"Your Name", identite.group("nom")
|
|
|
|
|
|
).replace("your@email.com", identite.group("courriel"))
|
|
|
|
|
|
with open(dest_file, "w", encoding="utf-8") as fh:
|
|
|
|
|
|
fh.write(content)
|
|
|
|
|
|
except OSError as e:
|
|
|
|
|
|
print(f"{t('Error creating file: ')}{e}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
print(f"{t('File created successfully: ')}{dest_file}")
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
2026-03-12 04:37:19 -04:00
|
|
|
|
def _list_claude_commands(self):
|
[IMP] todo claude configs: list missing and outdated commands, redeploy
The command list showed only what sat in ~/.claude/commands, with a date:
a command never installed stayed invisible, and a copy older than its
template in conf/ looked the same as a current one. Each ERPLibre template is
now compared with its copy, ignoring the git identity line: not installed,
up to date, or to redeploy with its +/- line count; other commands are
marked not from ERPLibre. On y/N it shows the diff, then redeploys the
outdated copies, keeping the name and e-mail /commit carried.
Checked: 7 new tests on a temporary directory; test_todo, 81 tests, passes.
--- FR ---
[IMP] todo configs claude : commandes absentes, périmées, redéploiement
La liste des commandes ne montrait que le contenu de ~/.claude/commands,
avec une date : une commande jamais installée restait invisible, et une copie
plus vieille que son gabarit de conf/ ressemblait à une copie à jour. Chaque
gabarit d'ERPLibre est désormais comparé à sa copie, hors ligne d'identité
git : non installée, à jour, ou à redéployer avec son compte +/- ; les autres
commandes sont marquées hors ERPLibre. Sur o/N elle montre le diff, puis
redéploie les copies périmées en gardant le nom et le courriel de /commit.
Vérifié : 7 nouveaux tests sur un dossier temporaire ; test_todo, 81 tests.
Assisted-by: Claude Opus 5.5
2026-09-25 01:38:25 -04:00
|
|
|
|
"""Les commandes `/…` : installées ou non, à jour ou à redéployer.
|
|
|
|
|
|
|
|
|
|
|
|
Chaque gabarit d'ERPLibre est comparé à sa copie de
|
|
|
|
|
|
~/.claude/commands ; une commande installée qui n'en vient pas est
|
|
|
|
|
|
listée à part. Propose ensuite le diff des copies périmées, puis leur
|
|
|
|
|
|
redéploiement, chaque fois sur un oui explicite.
|
|
|
|
|
|
"""
|
|
|
|
|
|
commands_dir = os.path.expanduser(self._CLAUDE_COMMANDS_DIR)
|
|
|
|
|
|
installees = set()
|
|
|
|
|
|
if os.path.isdir(commands_dir):
|
|
|
|
|
|
installees = {
|
|
|
|
|
|
f[:-3] for f in os.listdir(commands_dir) if f.endswith(".md")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def date_de(nom):
|
|
|
|
|
|
mtime = os.path.getmtime(os.path.join(commands_dir, f"{nom}.md"))
|
|
|
|
|
|
return datetime.datetime.fromtimestamp(mtime).strftime(
|
|
|
|
|
|
"%Y-%m-%d %H:%M"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
largeur = 70
|
|
|
|
|
|
print(t("Claude Code custom commands:"))
|
|
|
|
|
|
print("-" * largeur)
|
|
|
|
|
|
perimees, absentes = [], []
|
|
|
|
|
|
for nom, gabarit in sorted(self._CLAUDE_COMMAND_TEMPLATES.items()):
|
|
|
|
|
|
modele = self._claude_template_path(gabarit)
|
|
|
|
|
|
if nom not in installees:
|
|
|
|
|
|
absentes.append(nom)
|
|
|
|
|
|
etat = t("command not installed")
|
|
|
|
|
|
print(f" /{nom:<26} {'-':<16} {etat}")
|
|
|
|
|
|
continue
|
|
|
|
|
|
copie = os.path.join(commands_dir, f"{nom}.md")
|
|
|
|
|
|
if not os.path.isfile(modele):
|
|
|
|
|
|
etat = t("not in the repository")
|
|
|
|
|
|
else:
|
|
|
|
|
|
diff = self._claude_command_diff(copie, modele)
|
|
|
|
|
|
if diff:
|
|
|
|
|
|
perimees.append((nom, diff))
|
|
|
|
|
|
plus, moins = self._compte_diff(diff)
|
|
|
|
|
|
etat = f"{t('redeploy needed')} (+{plus} -{moins})"
|
|
|
|
|
|
else:
|
|
|
|
|
|
etat = t("up to date")
|
|
|
|
|
|
print(f" /{nom:<26} {date_de(nom):<16} {etat}")
|
|
|
|
|
|
|
|
|
|
|
|
autres = sorted(installees - set(self._CLAUDE_COMMAND_TEMPLATES))
|
|
|
|
|
|
for nom in autres:
|
|
|
|
|
|
etat = t("not from ERPLibre")
|
|
|
|
|
|
print(f" /{nom:<26} {date_de(nom):<16} {etat}")
|
|
|
|
|
|
print("-" * largeur)
|
|
|
|
|
|
print(
|
|
|
|
|
|
t("Total: %s installed, %s to redeploy, %s not installed")
|
|
|
|
|
|
% (len(installees), len(perimees), len(absentes))
|
|
|
|
|
|
)
|
|
|
|
|
|
if absentes:
|
|
|
|
|
|
print(t("A command not installed is deployed from this menu."))
|
|
|
|
|
|
|
|
|
|
|
|
if not perimees:
|
2026-03-10 02:05:53 -04:00
|
|
|
|
return
|
[IMP] todo claude configs: list missing and outdated commands, redeploy
The command list showed only what sat in ~/.claude/commands, with a date:
a command never installed stayed invisible, and a copy older than its
template in conf/ looked the same as a current one. Each ERPLibre template is
now compared with its copy, ignoring the git identity line: not installed,
up to date, or to redeploy with its +/- line count; other commands are
marked not from ERPLibre. On y/N it shows the diff, then redeploys the
outdated copies, keeping the name and e-mail /commit carried.
Checked: 7 new tests on a temporary directory; test_todo, 81 tests, passes.
--- FR ---
[IMP] todo configs claude : commandes absentes, périmées, redéploiement
La liste des commandes ne montrait que le contenu de ~/.claude/commands,
avec une date : une commande jamais installée restait invisible, et une copie
plus vieille que son gabarit de conf/ ressemblait à une copie à jour. Chaque
gabarit d'ERPLibre est désormais comparé à sa copie, hors ligne d'identité
git : non installée, à jour, ou à redéployer avec son compte +/- ; les autres
commandes sont marquées hors ERPLibre. Sur o/N elle montre le diff, puis
redéploie les copies périmées en gardant le nom et le courriel de /commit.
Vérifié : 7 nouveaux tests sur un dossier temporaire ; test_todo, 81 tests.
Assisted-by: Claude Opus 5.5
2026-09-25 01:38:25 -04:00
|
|
|
|
print()
|
|
|
|
|
|
if self._is_yes(input(t("Show the differences? [o/N] "))):
|
|
|
|
|
|
for nom, diff in perimees:
|
|
|
|
|
|
print(f"\n/{nom}")
|
|
|
|
|
|
for ligne in diff:
|
|
|
|
|
|
print(ligne.rstrip("\n"))
|
|
|
|
|
|
print()
|
|
|
|
|
|
question = t("Redeploy the %s outdated command(s)? [o/N] ") % len(
|
|
|
|
|
|
perimees
|
2026-03-12 04:37:19 -04:00
|
|
|
|
)
|
[IMP] todo claude configs: list missing and outdated commands, redeploy
The command list showed only what sat in ~/.claude/commands, with a date:
a command never installed stayed invisible, and a copy older than its
template in conf/ looked the same as a current one. Each ERPLibre template is
now compared with its copy, ignoring the git identity line: not installed,
up to date, or to redeploy with its +/- line count; other commands are
marked not from ERPLibre. On y/N it shows the diff, then redeploys the
outdated copies, keeping the name and e-mail /commit carried.
Checked: 7 new tests on a temporary directory; test_todo, 81 tests, passes.
--- FR ---
[IMP] todo configs claude : commandes absentes, périmées, redéploiement
La liste des commandes ne montrait que le contenu de ~/.claude/commands,
avec une date : une commande jamais installée restait invisible, et une copie
plus vieille que son gabarit de conf/ ressemblait à une copie à jour. Chaque
gabarit d'ERPLibre est désormais comparé à sa copie, hors ligne d'identité
git : non installée, à jour, ou à redéployer avec son compte +/- ; les autres
commandes sont marquées hors ERPLibre. Sur o/N elle montre le diff, puis
redéploie les copies périmées en gardant le nom et le courriel de /commit.
Vérifié : 7 nouveaux tests sur un dossier temporaire ; test_todo, 81 tests.
Assisted-by: Claude Opus 5.5
2026-09-25 01:38:25 -04:00
|
|
|
|
if not self._is_yes(input(question)):
|
|
|
|
|
|
print(t("Nothing to do."))
|
2026-03-12 04:37:19 -04:00
|
|
|
|
return
|
[IMP] todo claude configs: list missing and outdated commands, redeploy
The command list showed only what sat in ~/.claude/commands, with a date:
a command never installed stayed invisible, and a copy older than its
template in conf/ looked the same as a current one. Each ERPLibre template is
now compared with its copy, ignoring the git identity line: not installed,
up to date, or to redeploy with its +/- line count; other commands are
marked not from ERPLibre. On y/N it shows the diff, then redeploys the
outdated copies, keeping the name and e-mail /commit carried.
Checked: 7 new tests on a temporary directory; test_todo, 81 tests, passes.
--- FR ---
[IMP] todo configs claude : commandes absentes, périmées, redéploiement
La liste des commandes ne montrait que le contenu de ~/.claude/commands,
avec une date : une commande jamais installée restait invisible, et une copie
plus vieille que son gabarit de conf/ ressemblait à une copie à jour. Chaque
gabarit d'ERPLibre est désormais comparé à sa copie, hors ligne d'identité
git : non installée, à jour, ou à redéployer avec son compte +/- ; les autres
commandes sont marquées hors ERPLibre. Sur o/N elle montre le diff, puis
redéploie les copies périmées en gardant le nom et le courriel de /commit.
Vérifié : 7 nouveaux tests sur un dossier temporaire ; test_todo, 81 tests.
Assisted-by: Claude Opus 5.5
2026-09-25 01:38:25 -04:00
|
|
|
|
for nom, _ in perimees:
|
|
|
|
|
|
self._redeploy_claude_command(
|
|
|
|
|
|
nom, self._CLAUDE_COMMAND_TEMPLATES[nom]
|
2026-03-12 04:37:19 -04:00
|
|
|
|
)
|
2026-03-10 02:05:53 -04:00
|
|
|
|
|
[ADD] commentaires : un relevé non bloquant, sa règle, le code nettoyé
Rien ne relevait les commentaires hors convention. L'outil lit commentaires et
docstrings, jamais le code autour, et rend deux familles inégales :
l'identifiant — adresse, courriel, chemin de compte, nom de la liste privée —
est une trouvaille ; le récit — témoignage, date, personne — un signal à relire.
Le hook pre-commit le lance sur l'index, sort toujours en 0 — un contrôle
bloquant à cette échelle se fait désinstaller — et parle quand l'outil échoue.
La règle du générateur et sa doc portent le nettoyage au fur et à mesure, et
l'exemple d'un interdit s'invente : base, adresse, compte et hôte en prennent un.
L'écran du contexte est posé, sans entrée de menu. Vérifié : 252 tests des six
fichiers d'essai touchés passent.
--- EN ---
Nothing reported the comments that break the convention. The tool reads
comments and docstrings, never the code around them, and returns two unequal
families: identifying data — address, e-mail, account path, private-list name
— is a finding; narrative — witness marker, date, person — a signal to re-read.
The pre-commit hook runs it on the index, always exits 0 — a blocking check at
that scale gets uninstalled — and speaks when the tool fails. The generator
rule and its doc carry the clean-up as you go, and a forbidden thing's example
is invented: a real database, address, account and host each take one.
The context screen is in place, with no menu entry. Checked: 252 tests of the
six touched fixture files pass.
Assisted-by: Claude Opus 5
2026-08-30 02:02:51 -04:00
|
|
|
|
def _claude_context_root(self):
|
|
|
|
|
|
"""La racine du dépôt, deux niveaux au-dessus de ce fichier."""
|
|
|
|
|
|
return os.path.normpath(
|
|
|
|
|
|
os.path.join(os.path.dirname(__file__), "..", "..")
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _compte_lignes(self, chemin):
|
|
|
|
|
|
"""Le nombre de lignes d'un fichier ; 0 s'il est illisible."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(chemin, encoding="utf-8", errors="replace") as fh:
|
|
|
|
|
|
return sum(1 for _ in fh)
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
def _claude_command_state(self, deployed, template):
|
|
|
|
|
|
"""La copie déployée d'une commande suit-elle encore le gabarit ?
|
|
|
|
|
|
|
|
|
|
|
|
La comparaison ignore les lignes qui portent une identité git : le
|
|
|
|
|
|
déploiement y substitue le nom et le courriel, et une égalité stricte
|
|
|
|
|
|
déclarerait périmée toute commande personnalisée.
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not os.path.isfile(template):
|
|
|
|
|
|
return t("not in the repository")
|
|
|
|
|
|
if not os.path.isfile(deployed):
|
|
|
|
|
|
return t("missing")
|
|
|
|
|
|
try:
|
[IMP] todo claude configs: list missing and outdated commands, redeploy
The command list showed only what sat in ~/.claude/commands, with a date:
a command never installed stayed invisible, and a copy older than its
template in conf/ looked the same as a current one. Each ERPLibre template is
now compared with its copy, ignoring the git identity line: not installed,
up to date, or to redeploy with its +/- line count; other commands are
marked not from ERPLibre. On y/N it shows the diff, then redeploys the
outdated copies, keeping the name and e-mail /commit carried.
Checked: 7 new tests on a temporary directory; test_todo, 81 tests, passes.
--- FR ---
[IMP] todo configs claude : commandes absentes, périmées, redéploiement
La liste des commandes ne montrait que le contenu de ~/.claude/commands,
avec une date : une commande jamais installée restait invisible, et une copie
plus vieille que son gabarit de conf/ ressemblait à une copie à jour. Chaque
gabarit d'ERPLibre est désormais comparé à sa copie, hors ligne d'identité
git : non installée, à jour, ou à redéployer avec son compte +/- ; les autres
commandes sont marquées hors ERPLibre. Sur o/N elle montre le diff, puis
redéploie les copies périmées en gardant le nom et le courriel de /commit.
Vérifié : 7 nouveaux tests sur un dossier temporaire ; test_todo, 81 tests.
Assisted-by: Claude Opus 5.5
2026-09-25 01:38:25 -04:00
|
|
|
|
if not self._claude_command_diff(deployed, template):
|
[ADD] commentaires : un relevé non bloquant, sa règle, le code nettoyé
Rien ne relevait les commentaires hors convention. L'outil lit commentaires et
docstrings, jamais le code autour, et rend deux familles inégales :
l'identifiant — adresse, courriel, chemin de compte, nom de la liste privée —
est une trouvaille ; le récit — témoignage, date, personne — un signal à relire.
Le hook pre-commit le lance sur l'index, sort toujours en 0 — un contrôle
bloquant à cette échelle se fait désinstaller — et parle quand l'outil échoue.
La règle du générateur et sa doc portent le nettoyage au fur et à mesure, et
l'exemple d'un interdit s'invente : base, adresse, compte et hôte en prennent un.
L'écran du contexte est posé, sans entrée de menu. Vérifié : 252 tests des six
fichiers d'essai touchés passent.
--- EN ---
Nothing reported the comments that break the convention. The tool reads
comments and docstrings, never the code around them, and returns two unequal
families: identifying data — address, e-mail, account path, private-list name
— is a finding; narrative — witness marker, date, person — a signal to re-read.
The pre-commit hook runs it on the index, always exits 0 — a blocking check at
that scale gets uninstalled — and speaks when the tool fails. The generator
rule and its doc carry the clean-up as you go, and a forbidden thing's example
is invented: a real database, address, account and host each take one.
The context screen is in place, with no menu entry. Checked: 252 tests of the
six touched fixture files pass.
Assisted-by: Claude Opus 5
2026-08-30 02:02:51 -04:00
|
|
|
|
return t("up to date")
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
return t("missing")
|
|
|
|
|
|
return t("redeploy needed")
|
|
|
|
|
|
|
|
|
|
|
|
def _claude_memory_dir(self):
|
|
|
|
|
|
"""Le répertoire de mémoire de Claude Code pour CE dépôt.
|
|
|
|
|
|
|
|
|
|
|
|
Le nom du projet est le chemin absolu dont chaque séparateur devient
|
|
|
|
|
|
un tiret : c'est la convention de Claude Code, pas la nôtre.
|
|
|
|
|
|
"""
|
|
|
|
|
|
racine = self._claude_context_root()
|
|
|
|
|
|
projet = racine.replace(os.sep, "-")
|
|
|
|
|
|
return os.path.expanduser(
|
|
|
|
|
|
os.path.join("~/.claude/projects", projet, "memory")
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _show_claude_context(self):
|
|
|
|
|
|
"""Ce que Claude reçoit avant la première question : sources et état.
|
|
|
|
|
|
|
|
|
|
|
|
Rend None. Écrit un tableau et ne modifie rien. Ne relève que ce qui
|
|
|
|
|
|
est versionné ou déployé ; ce que `private/` contient n'y figure pas.
|
|
|
|
|
|
"""
|
|
|
|
|
|
racine = self._claude_context_root()
|
|
|
|
|
|
largeur = 62
|
|
|
|
|
|
print(f"🧠 {t('Context given to Claude')}")
|
|
|
|
|
|
print("-" * largeur)
|
|
|
|
|
|
|
|
|
|
|
|
instructions = os.path.join(racine, "CLAUDE.md")
|
|
|
|
|
|
if os.path.isfile(instructions):
|
|
|
|
|
|
n = self._compte_lignes(instructions)
|
|
|
|
|
|
print(f"{t('Instructions'):<22} CLAUDE.md {n} {t('lines')}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"{t('Instructions'):<22} CLAUDE.md {t('missing')}")
|
|
|
|
|
|
|
|
|
|
|
|
regles = os.path.join(racine, ".claude", "rules")
|
|
|
|
|
|
if os.path.isdir(regles):
|
|
|
|
|
|
noms = sorted(f for f in os.listdir(regles) if f.endswith(".md"))
|
|
|
|
|
|
total = sum(
|
|
|
|
|
|
self._compte_lignes(os.path.join(regles, f)) for f in noms
|
|
|
|
|
|
)
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"{t('Rules'):<22} .claude/rules/ {len(noms)}"
|
|
|
|
|
|
f" {t('files')}, {total} {t('lines')}"
|
|
|
|
|
|
)
|
|
|
|
|
|
for nom in noms:
|
|
|
|
|
|
n = self._compte_lignes(os.path.join(regles, nom))
|
|
|
|
|
|
print(f"{'':<22} {nom:<28} {n} {t('lines')}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"{t('Rules'):<22} .claude/rules/ {t('missing')}")
|
|
|
|
|
|
|
|
|
|
|
|
skills = os.path.join(racine, ".claude", "skills")
|
|
|
|
|
|
if os.path.isdir(skills):
|
|
|
|
|
|
noms = sorted(
|
|
|
|
|
|
d
|
|
|
|
|
|
for d in os.listdir(skills)
|
|
|
|
|
|
if os.path.isfile(os.path.join(skills, d, "SKILL.md"))
|
|
|
|
|
|
)
|
|
|
|
|
|
print(f"{t('Skills'):<22} .claude/skills/ {len(noms)}")
|
|
|
|
|
|
for nom in noms:
|
|
|
|
|
|
print(f"{'':<22} {nom}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"{t('Skills'):<22} .claude/skills/ {t('missing')}")
|
|
|
|
|
|
|
|
|
|
|
|
print(f"{t('Deployed commands'):<22} ~/.claude/commands/")
|
[IMP] todo claude configs: list missing and outdated commands, redeploy
The command list showed only what sat in ~/.claude/commands, with a date:
a command never installed stayed invisible, and a copy older than its
template in conf/ looked the same as a current one. Each ERPLibre template is
now compared with its copy, ignoring the git identity line: not installed,
up to date, or to redeploy with its +/- line count; other commands are
marked not from ERPLibre. On y/N it shows the diff, then redeploys the
outdated copies, keeping the name and e-mail /commit carried.
Checked: 7 new tests on a temporary directory; test_todo, 81 tests, passes.
--- FR ---
[IMP] todo configs claude : commandes absentes, périmées, redéploiement
La liste des commandes ne montrait que le contenu de ~/.claude/commands,
avec une date : une commande jamais installée restait invisible, et une copie
plus vieille que son gabarit de conf/ ressemblait à une copie à jour. Chaque
gabarit d'ERPLibre est désormais comparé à sa copie, hors ligne d'identité
git : non installée, à jour, ou à redéployer avec son compte +/- ; les autres
commandes sont marquées hors ERPLibre. Sur o/N elle montre le diff, puis
redéploie les copies périmées en gardant le nom et le courriel de /commit.
Vérifié : 7 nouveaux tests sur un dossier temporaire ; test_todo, 81 tests.
Assisted-by: Claude Opus 5.5
2026-09-25 01:38:25 -04:00
|
|
|
|
gabarits = self._CLAUDE_COMMAND_TEMPLATES
|
[ADD] commentaires : un relevé non bloquant, sa règle, le code nettoyé
Rien ne relevait les commentaires hors convention. L'outil lit commentaires et
docstrings, jamais le code autour, et rend deux familles inégales :
l'identifiant — adresse, courriel, chemin de compte, nom de la liste privée —
est une trouvaille ; le récit — témoignage, date, personne — un signal à relire.
Le hook pre-commit le lance sur l'index, sort toujours en 0 — un contrôle
bloquant à cette échelle se fait désinstaller — et parle quand l'outil échoue.
La règle du générateur et sa doc portent le nettoyage au fur et à mesure, et
l'exemple d'un interdit s'invente : base, adresse, compte et hôte en prennent un.
L'écran du contexte est posé, sans entrée de menu. Vérifié : 252 tests des six
fichiers d'essai touchés passent.
--- EN ---
Nothing reported the comments that break the convention. The tool reads
comments and docstrings, never the code around them, and returns two unequal
families: identifying data — address, e-mail, account path, private-list name
— is a finding; narrative — witness marker, date, person — a signal to re-read.
The pre-commit hook runs it on the index, always exits 0 — a blocking check at
that scale gets uninstalled — and speaks when the tool fails. The generator
rule and its doc carry the clean-up as you go, and a forbidden thing's example
is invented: a real database, address, account and host each take one.
The context screen is in place, with no menu entry. Checked: 252 tests of the
six touched fixture files pass.
Assisted-by: Claude Opus 5
2026-08-30 02:02:51 -04:00
|
|
|
|
for nom, gabarit in sorted(gabarits.items()):
|
|
|
|
|
|
etat = self._claude_command_state(
|
|
|
|
|
|
os.path.expanduser(f"~/.claude/commands/{nom}.md"),
|
|
|
|
|
|
os.path.join(racine, "conf", gabarit),
|
|
|
|
|
|
)
|
|
|
|
|
|
print(f"{'':<22} /{nom:<26} {etat}")
|
|
|
|
|
|
|
|
|
|
|
|
chemin_hooks = self._git_hooks_path(racine)
|
|
|
|
|
|
print(
|
[ADD] déploiement : un proxy SOCKS par SSH, et son mode d'emploi
« -L » relaie UN service ; « -D » ouvre un relais SOCKS, par lequel le
navigateur atteint n'importe quelle destination depuis la machine distante —
une interface qui n'écoute que sur sa boucle locale, un hôte de son réseau sans
route depuis ici. Le port par défaut est 1080, modifiable, et contrôlé libre
avant d'ouvrir. La saisie de l'adresse est celle de sshfs, désormais partagée :
l'alias de ~/.ssh/config part tel quel à ssh, faute de quoi son ProxyJump se
perdrait et une VM imbriquée deviendrait injoignable. Le réglage de Firefox
s'affiche AVANT le lancement, la commande ne rendant la main qu'au Ctrl+C.
Vérifié : 9 tests neufs, et la numérotation du menu que deux autres gardent.
--- EN ---
"-L" relays ONE service; "-D" opens a SOCKS relay, through which the browser
reaches any destination from the remote machine — an interface listening only
on its loopback, a host of its network with no route from here. The default
port is 1080, changeable, and checked free before opening. The address prompt
is sshfs's, now shared: the ~/.ssh/config alias goes to ssh as is, failing
which its ProxyJump would be lost and a nested VM become unreachable. The
Firefox settings print BEFORE the launch, the command only returning on
Ctrl+C.
Checked: 9 new tests, and the menu numbering two others guard.
Assisted-by: Claude Opus 5
2026-09-24 13:32:28 -04:00
|
|
|
|
f"{t('Git hooks'):<22} {chemin_hooks or t('hook not installed')}"
|
[ADD] commentaires : un relevé non bloquant, sa règle, le code nettoyé
Rien ne relevait les commentaires hors convention. L'outil lit commentaires et
docstrings, jamais le code autour, et rend deux familles inégales :
l'identifiant — adresse, courriel, chemin de compte, nom de la liste privée —
est une trouvaille ; le récit — témoignage, date, personne — un signal à relire.
Le hook pre-commit le lance sur l'index, sort toujours en 0 — un contrôle
bloquant à cette échelle se fait désinstaller — et parle quand l'outil échoue.
La règle du générateur et sa doc portent le nettoyage au fur et à mesure, et
l'exemple d'un interdit s'invente : base, adresse, compte et hôte en prennent un.
L'écran du contexte est posé, sans entrée de menu. Vérifié : 252 tests des six
fichiers d'essai touchés passent.
--- EN ---
Nothing reported the comments that break the convention. The tool reads
comments and docstrings, never the code around them, and returns two unequal
families: identifying data — address, e-mail, account path, private-list name
— is a finding; narrative — witness marker, date, person — a signal to re-read.
The pre-commit hook runs it on the index, always exits 0 — a blocking check at
that scale gets uninstalled — and speaks when the tool fails. The generator
rule and its doc carry the clean-up as you go, and a forbidden thing's example
is invented: a real database, address, account and host each take one.
The context screen is in place, with no menu entry. Checked: 252 tests of the
six touched fixture files pass.
Assisted-by: Claude Opus 5
2026-08-30 02:02:51 -04:00
|
|
|
|
)
|
|
|
|
|
|
if chemin_hooks:
|
|
|
|
|
|
absolu = os.path.join(racine, chemin_hooks)
|
2026-08-31 06:17:48 -04:00
|
|
|
|
for hook in self._GIT_HOOKS:
|
[ADD] commentaires : un relevé non bloquant, sa règle, le code nettoyé
Rien ne relevait les commentaires hors convention. L'outil lit commentaires et
docstrings, jamais le code autour, et rend deux familles inégales :
l'identifiant — adresse, courriel, chemin de compte, nom de la liste privée —
est une trouvaille ; le récit — témoignage, date, personne — un signal à relire.
Le hook pre-commit le lance sur l'index, sort toujours en 0 — un contrôle
bloquant à cette échelle se fait désinstaller — et parle quand l'outil échoue.
La règle du générateur et sa doc portent le nettoyage au fur et à mesure, et
l'exemple d'un interdit s'invente : base, adresse, compte et hôte en prennent un.
L'écran du contexte est posé, sans entrée de menu. Vérifié : 252 tests des six
fichiers d'essai touchés passent.
--- EN ---
Nothing reported the comments that break the convention. The tool reads
comments and docstrings, never the code around them, and returns two unequal
families: identifying data — address, e-mail, account path, private-list name
— is a finding; narrative — witness marker, date, person — a signal to re-read.
The pre-commit hook runs it on the index, always exits 0 — a blocking check at
that scale gets uninstalled — and speaks when the tool fails. The generator
rule and its doc carry the clean-up as you go, and a forbidden thing's example
is invented: a real database, address, account and host each take one.
The context screen is in place, with no menu entry. Checked: 252 tests of the
six touched fixture files pass.
Assisted-by: Claude Opus 5
2026-08-30 02:02:51 -04:00
|
|
|
|
pose = os.access(os.path.join(absolu, hook), os.X_OK)
|
|
|
|
|
|
marque = (
|
|
|
|
|
|
t("hook installed") if pose else t("hook not installed")
|
|
|
|
|
|
)
|
|
|
|
|
|
print(f"{'':<22} {hook:<26} {marque}")
|
|
|
|
|
|
|
|
|
|
|
|
memoire = self._claude_memory_dir()
|
|
|
|
|
|
if os.path.isdir(memoire):
|
|
|
|
|
|
n = len([f for f in os.listdir(memoire) if f.endswith(".md")])
|
|
|
|
|
|
print(f"{t('Memory'):<22} ~/.claude/projects/…/memory/ {n}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"{t('Memory'):<22} {t('missing')}")
|
|
|
|
|
|
|
|
|
|
|
|
print("-" * largeur)
|
|
|
|
|
|
|
|
|
|
|
|
def _git_hooks_path(self, racine):
|
|
|
|
|
|
"""La valeur de core.hooksPath, ou None si git n'en déclare aucune."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
sortie = subprocess.run(
|
|
|
|
|
|
["git", "config", "--get", "core.hooksPath"],
|
|
|
|
|
|
capture_output=True,
|
|
|
|
|
|
text=True,
|
|
|
|
|
|
cwd=racine,
|
|
|
|
|
|
timeout=10,
|
|
|
|
|
|
)
|
|
|
|
|
|
except (OSError, subprocess.SubprocessError):
|
|
|
|
|
|
return None
|
|
|
|
|
|
chemin = sortie.stdout.strip()
|
|
|
|
|
|
return chemin or None
|
|
|
|
|
|
|
2026-08-07 03:22:26 -04:00
|
|
|
|
def _setup_claude_command(
|
|
|
|
|
|
self, command_name, template_filename, personalize=False
|
|
|
|
|
|
):
|
|
|
|
|
|
dest_dir = os.path.expanduser("~/.claude/commands")
|
|
|
|
|
|
dest_file = os.path.join(dest_dir, f"{command_name}.md")
|
|
|
|
|
|
|
|
|
|
|
|
if os.path.exists(dest_file):
|
|
|
|
|
|
print(f"{t('File already exists: ')}{dest_file}")
|
|
|
|
|
|
overwrite = input(t("Do you want to overwrite the file? (y/Y): "))
|
|
|
|
|
|
if not self._is_yes(overwrite):
|
|
|
|
|
|
print(t("Nothing to do."))
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
template_path = os.path.join(
|
|
|
|
|
|
os.path.dirname(__file__),
|
|
|
|
|
|
"..",
|
|
|
|
|
|
"..",
|
|
|
|
|
|
"conf",
|
|
|
|
|
|
template_filename,
|
|
|
|
|
|
)
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(template_path) as f:
|
|
|
|
|
|
content = f.read()
|
|
|
|
|
|
|
|
|
|
|
|
if personalize:
|
|
|
|
|
|
name = input(t("Enter your full name: ")).strip()
|
|
|
|
|
|
email = input(t("Enter your email: ")).strip()
|
|
|
|
|
|
content = content.replace("your@email.com", email)
|
|
|
|
|
|
content = content.replace("Your Name", name)
|
|
|
|
|
|
|
|
|
|
|
|
os.makedirs(dest_dir, exist_ok=True)
|
|
|
|
|
|
with open(dest_file, "w") as f:
|
|
|
|
|
|
f.write(content)
|
|
|
|
|
|
|
|
|
|
|
|
print(f"{t('File created successfully: ')}{dest_file}")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"{t('Error creating file: ')}{e}")
|
|
|
|
|
|
|
2026-03-12 04:04:52 -04:00
|
|
|
|
def _claude_add_automation(self):
|
2026-03-13 15:04:14 -04:00
|
|
|
|
description = input(t("Description of the command to add: ")).strip()
|
2026-03-12 04:04:52 -04:00
|
|
|
|
if not description:
|
|
|
|
|
|
return
|
2026-03-13 15:04:14 -04:00
|
|
|
|
command = input(t("Bash command to execute: ")).strip()
|
2026-03-12 04:04:52 -04:00
|
|
|
|
if not command:
|
|
|
|
|
|
return
|
|
|
|
|
|
section = (
|
|
|
|
|
|
input(
|
2026-03-12 05:31:45 -04:00
|
|
|
|
t("Menu section (git/code/config/network/process): ")
|
2026-03-12 04:04:52 -04:00
|
|
|
|
).strip()
|
|
|
|
|
|
or "git"
|
|
|
|
|
|
)
|
|
|
|
|
|
section_key = f"{section}_from_makefile"
|
2026-03-13 15:04:14 -04:00
|
|
|
|
config_path = os.path.join(os.path.dirname(__file__), "todo.json")
|
2026-03-12 04:04:52 -04:00
|
|
|
|
try:
|
|
|
|
|
|
with open(config_path) as f:
|
|
|
|
|
|
config = json.load(f)
|
|
|
|
|
|
if section_key not in config:
|
|
|
|
|
|
config[section_key] = []
|
|
|
|
|
|
config[section_key].append(
|
|
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": description,
|
|
|
|
|
|
"bash_command": command,
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
with open(config_path, "w") as f:
|
|
|
|
|
|
json.dump(config, f, indent=4, ensure_ascii=False)
|
|
|
|
|
|
f.write("\n")
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(t("Automation added successfully in todo.json!"))
|
2026-03-12 04:04:52 -04:00
|
|
|
|
except Exception as e:
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(f"{t('Error adding automation: ')}{e}")
|
2026-03-12 04:04:52 -04:00
|
|
|
|
|
[ADD] script todo : gérer les plugins Claude Code et leur liste ERPLibre
Rien ne posait un plugin depuis TODO ; la CLI seule le faisait, hors du
menu. L'installation passe par « -y » : la sortie de TODO est un tuyau,
pas un terminal, et la CLI refuse sans lui toute installation qui
exécute une commande déclarée par un marketplace. La liste préférée
s'affiche donc AVANT la confirmation, seule occasion de la lire ; ses
quatre plugins travaillent sur le poste, sans service tiers ni compte.
La recherche lit les manifestes sur le disque, donc hors ligne.
Vérifié : 8 tests neufs, dont la frontière de mot qui sépare deux noms
dont l'un contient l'autre — une recherche naïve en rate trois.
--- EN ---
Nothing installed a plugin from TODO; the CLI alone did, outside the
menu. Installing goes through « -y »: TODO's output is a pipe, not a
terminal, and without it the CLI refuses any install that runs a
command declared by a marketplace. The preferred list is therefore
shown BEFORE the confirmation, the only chance to read it; its four
plugins run on the workstation, with no third party and no account.
Search reads the manifests from disk, hence offline.
Checked: 8 new tests, among them the word boundary separating two names
where one contains the other — a naive search misses three of them.
Assisted-by: Claude Opus 5
2026-09-02 06:35:05 -04:00
|
|
|
|
# Les plugins qu'ERPLibre pose par défaut, chacun avec la clé qui dit à
|
|
|
|
|
|
# quoi il sert. Tous viennent du marketplace officiel et travaillent sur
|
|
|
|
|
|
# le poste : aucun n'appelle un service tiers ni ne réclame de compte.
|
|
|
|
|
|
_CLAUDE_PREFERRED_PLUGINS = (
|
|
|
|
|
|
("superpowers", "brainstorming, subagent-driven development, TDD"),
|
|
|
|
|
|
("pyright-lsp", "Python type checking and code intelligence"),
|
|
|
|
|
|
("claude-security", "vulnerability scan run entirely in session"),
|
|
|
|
|
|
(
|
|
|
|
|
|
"skill-creator",
|
|
|
|
|
|
"write, improve and evaluate the repository skills",
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
_CLAUDE_MARKETPLACES_DIR = "~/.claude/plugins/marketplaces"
|
|
|
|
|
|
|
|
|
|
|
|
def prompt_execute_claude_plugins(self):
|
|
|
|
|
|
print(f"🤖 {t('Manage Claude Code plugins and marketplaces!')}")
|
|
|
|
|
|
choices = [
|
|
|
|
|
|
{"section": t("Inventory")},
|
|
|
|
|
|
{"prompt_description": t("List installed plugins")},
|
|
|
|
|
|
{"prompt_description": t("List configured marketplaces")},
|
|
|
|
|
|
{"prompt_description": t("Search a plugin in the marketplaces")},
|
|
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
|
"Show a plugin detail and its token cost"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
[FIX] script todo : une clé i18n dupliquée écrasait un libellé du menu
La section du menu des plugins prenait « Install » pour clé, déjà portée
par l'entrée « 📦 Installation » du menu principal. Dans un littéral de
dict Python la dernière définition gagne, sans erreur ni avertissement :
le menu principal affichait donc « Installer ». La clé de la section est
renommée, la première étant en place depuis longtemps.
Vérifié : le menu principal retrouve « 📦 Installation », et un balayage
des clés ne laisse que trois doublons, tous à valeurs identiques donc
sans effet.
--- EN ---
The plugin menu's section used « Install » as its key, already held by
the main menu's « 📦 Installation » entry. In a Python dict literal the
last definition wins, with no error and no warning: the main menu
therefore showed « Installer ». The section's key is renamed, the first
one having been there far longer.
Checked: the main menu gets « 📦 Installation » back, and a sweep of the
keys leaves only three duplicates, all with identical values and so
without effect.
Assisted-by: Claude Opus 5
2026-09-03 01:23:03 -04:00
|
|
|
|
{"section": t("Install plugins")},
|
[ADD] script todo : gérer les plugins Claude Code et leur liste ERPLibre
Rien ne posait un plugin depuis TODO ; la CLI seule le faisait, hors du
menu. L'installation passe par « -y » : la sortie de TODO est un tuyau,
pas un terminal, et la CLI refuse sans lui toute installation qui
exécute une commande déclarée par un marketplace. La liste préférée
s'affiche donc AVANT la confirmation, seule occasion de la lire ; ses
quatre plugins travaillent sur le poste, sans service tiers ni compte.
La recherche lit les manifestes sur le disque, donc hors ligne.
Vérifié : 8 tests neufs, dont la frontière de mot qui sépare deux noms
dont l'un contient l'autre — une recherche naïve en rate trois.
--- EN ---
Nothing installed a plugin from TODO; the CLI alone did, outside the
menu. Installing goes through « -y »: TODO's output is a pipe, not a
terminal, and without it the CLI refuses any install that runs a
command declared by a marketplace. The preferred list is therefore
shown BEFORE the confirmation, the only chance to read it; its four
plugins run on the workstation, with no third party and no account.
Search reads the manifests from disk, hence offline.
Checked: 8 new tests, among them the word boundary separating two names
where one contains the other — a naive search misses three of them.
Assisted-by: Claude Opus 5
2026-09-02 06:35:05 -04:00
|
|
|
|
{"prompt_description": t("Install the ERPLibre preferred list")},
|
|
|
|
|
|
{"prompt_description": t("Install a plugin by name")},
|
|
|
|
|
|
{"prompt_description": t("Add a marketplace")},
|
|
|
|
|
|
{"section": t("Maintenance")},
|
|
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
|
"Update the marketplaces and the plugins"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
|
|
|
|
|
{"prompt_description": t("Uninstall a plugin")},
|
|
|
|
|
|
]
|
|
|
|
|
|
help_info = self.fill_help_info(choices)
|
|
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
|
print()
|
|
|
|
|
|
if status == "0":
|
|
|
|
|
|
return False
|
|
|
|
|
|
elif status == "1":
|
|
|
|
|
|
self._claude_plugin_exec("list")
|
|
|
|
|
|
elif status == "2":
|
|
|
|
|
|
self._claude_plugin_exec("marketplace list")
|
|
|
|
|
|
elif status == "3":
|
|
|
|
|
|
self._claude_plugin_search()
|
|
|
|
|
|
elif status == "4":
|
|
|
|
|
|
self._claude_plugin_details()
|
|
|
|
|
|
elif status == "5":
|
|
|
|
|
|
self._claude_install_preferred_plugins()
|
|
|
|
|
|
elif status == "6":
|
|
|
|
|
|
self._claude_plugin_install_by_name()
|
|
|
|
|
|
elif status == "7":
|
|
|
|
|
|
self._claude_marketplace_add()
|
|
|
|
|
|
elif status == "8":
|
|
|
|
|
|
self._claude_plugin_update()
|
|
|
|
|
|
elif status == "9":
|
|
|
|
|
|
self._claude_plugin_uninstall()
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(t("Command not found !"))
|
|
|
|
|
|
|
|
|
|
|
|
def _claude_plugin_exec(self, args, quiet=False, capture=False):
|
|
|
|
|
|
"""Lance « claude plugin <args> », ou signale que claude est absent.
|
|
|
|
|
|
|
|
|
|
|
|
Rend le code de sortie, ou le couple (code, lignes) quand capture est
|
|
|
|
|
|
vrai. Le code 1 sans sortie signale l'absence de l'exécutable : rien
|
|
|
|
|
|
n'a tourné, et l'appelant ne doit pas conclure à un échec de la
|
|
|
|
|
|
commande elle-même.
|
|
|
|
|
|
"""
|
|
|
|
|
|
claude = shutil.which("claude")
|
|
|
|
|
|
if claude is None:
|
|
|
|
|
|
print(t("The claude command is not in the PATH."))
|
|
|
|
|
|
return (1, []) if capture else 1
|
|
|
|
|
|
return self.execute.exec_command_live(
|
|
|
|
|
|
f"{shlex.quote(claude)} plugin {args}",
|
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
quiet=quiet,
|
|
|
|
|
|
return_status_and_output=capture,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _claude_plugin_is_installed(self, name):
|
|
|
|
|
|
"""Le plugin est-il déjà posé ?
|
|
|
|
|
|
|
|
|
|
|
|
La liste est lue telle que la CLI l'écrit, et le nom y est cherché
|
|
|
|
|
|
comme un mot entier : « code-review » ne doit pas se reconnaître dans
|
|
|
|
|
|
« pr-review-toolkit ». Un doute rend faux, et l'installation qui suit
|
|
|
|
|
|
est de toute façon idempotente.
|
|
|
|
|
|
"""
|
|
|
|
|
|
result = self._claude_plugin_exec("list", quiet=True, capture=True)
|
|
|
|
|
|
if not isinstance(result, tuple) or result[0] != 0:
|
|
|
|
|
|
return False
|
|
|
|
|
|
motif = re.compile(rf"(?<![\w-]){re.escape(name)}(?![\w-])")
|
|
|
|
|
|
return any(motif.search(ligne) for ligne in result[1])
|
|
|
|
|
|
|
|
|
|
|
|
def _claude_install_preferred_plugins(self):
|
|
|
|
|
|
"""Pose la liste préférée d'ERPLibre après confirmation.
|
|
|
|
|
|
|
|
|
|
|
|
L'installation passe par « -y » : la sortie de TODO est un tuyau, pas
|
|
|
|
|
|
un terminal, et la CLI refuse sans lui toute installation qui exécute
|
|
|
|
|
|
une commande déclarée par un marketplace. La liste est donc affichée
|
|
|
|
|
|
AVANT la confirmation, qui est la seule occasion de la lire.
|
|
|
|
|
|
"""
|
|
|
|
|
|
print(t("ERPLibre preferred plugins:"))
|
|
|
|
|
|
print("-" * 62)
|
|
|
|
|
|
for nom, raison in self._CLAUDE_PREFERRED_PLUGINS:
|
|
|
|
|
|
print(f" {nom:<18} {t(raison)}")
|
|
|
|
|
|
print("-" * 62)
|
|
|
|
|
|
if not self._is_yes(input(t("Install these plugins? (y/Y): "))):
|
|
|
|
|
|
print(t("Nothing to do."))
|
|
|
|
|
|
return
|
|
|
|
|
|
for nom, _ in self._CLAUDE_PREFERRED_PLUGINS:
|
|
|
|
|
|
print(f"\n📦 {nom}")
|
|
|
|
|
|
if self._claude_plugin_is_installed(nom):
|
|
|
|
|
|
print(t("Already installed, skipped."))
|
|
|
|
|
|
continue
|
|
|
|
|
|
self._claude_plugin_exec(f"install {shlex.quote(nom)} -y")
|
|
|
|
|
|
print(f"\n{t('A restart of Claude Code applies the change.')}")
|
|
|
|
|
|
|
|
|
|
|
|
def _claude_plugin_install_by_name(self):
|
|
|
|
|
|
nom = input(t("Plugin name: ")).strip()
|
|
|
|
|
|
if not nom:
|
|
|
|
|
|
print(t("Nothing to do."))
|
|
|
|
|
|
return
|
|
|
|
|
|
self._claude_plugin_exec(f"install {shlex.quote(nom)} -y")
|
|
|
|
|
|
print(t("A restart of Claude Code applies the change."))
|
|
|
|
|
|
|
|
|
|
|
|
def _claude_plugin_details(self):
|
|
|
|
|
|
nom = input(t("Plugin name: ")).strip()
|
|
|
|
|
|
if not nom:
|
|
|
|
|
|
print(t("Nothing to do."))
|
|
|
|
|
|
return
|
|
|
|
|
|
self._claude_plugin_exec(f"details {shlex.quote(nom)}")
|
|
|
|
|
|
|
|
|
|
|
|
def _claude_plugin_uninstall(self):
|
|
|
|
|
|
nom = input(t("Plugin name: ")).strip()
|
|
|
|
|
|
if not nom:
|
|
|
|
|
|
print(t("Nothing to do."))
|
|
|
|
|
|
return
|
|
|
|
|
|
if not self._is_yes(input(t("Uninstall this plugin? (y/Y): "))):
|
|
|
|
|
|
print(t("Nothing to do."))
|
|
|
|
|
|
return
|
|
|
|
|
|
self._claude_plugin_exec(f"uninstall {shlex.quote(nom)}")
|
|
|
|
|
|
print(t("A restart of Claude Code applies the change."))
|
|
|
|
|
|
|
|
|
|
|
|
def _claude_marketplace_add(self):
|
|
|
|
|
|
source = input(
|
|
|
|
|
|
t("Marketplace source (URL, path or owner/repo): ")
|
|
|
|
|
|
).strip()
|
|
|
|
|
|
if not source:
|
|
|
|
|
|
print(t("Nothing to do."))
|
|
|
|
|
|
return
|
|
|
|
|
|
self._claude_plugin_exec(f"marketplace add {shlex.quote(source)}")
|
|
|
|
|
|
|
|
|
|
|
|
def _claude_plugin_update(self):
|
|
|
|
|
|
"""Met à jour les marketplaces, puis les plugins déjà posés.
|
|
|
|
|
|
|
|
|
|
|
|
Les catalogues passent d'abord : « plugin update » installe la version
|
|
|
|
|
|
que le catalogue local annonce, et sur un catalogue périmé il ne fait
|
|
|
|
|
|
rien tout en sortant en 0.
|
|
|
|
|
|
"""
|
|
|
|
|
|
self._claude_plugin_exec("marketplace update")
|
|
|
|
|
|
for nom, _ in self._CLAUDE_PREFERRED_PLUGINS:
|
|
|
|
|
|
if self._claude_plugin_is_installed(nom):
|
|
|
|
|
|
self._claude_plugin_exec(f"update {shlex.quote(nom)}")
|
|
|
|
|
|
print(t("A restart of Claude Code applies the change."))
|
|
|
|
|
|
|
|
|
|
|
|
def _claude_marketplace_catalog(self):
|
|
|
|
|
|
"""Les plugins des marketplaces posés, en triplets (nom, source, mot).
|
|
|
|
|
|
|
|
|
|
|
|
Le catalogue est lu sur le disque plutôt que par la CLI : la recherche
|
|
|
|
|
|
reste possible hors ligne, et un marketplace dont le manifeste est
|
|
|
|
|
|
illisible est sauté sans faire échouer les autres.
|
|
|
|
|
|
"""
|
|
|
|
|
|
racine = os.path.expanduser(self._CLAUDE_MARKETPLACES_DIR)
|
|
|
|
|
|
catalogue = []
|
|
|
|
|
|
if not os.path.isdir(racine):
|
|
|
|
|
|
return catalogue
|
|
|
|
|
|
for nom_marche in sorted(os.listdir(racine)):
|
|
|
|
|
|
manifeste = os.path.join(
|
|
|
|
|
|
racine, nom_marche, ".claude-plugin", "marketplace.json"
|
|
|
|
|
|
)
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(manifeste, encoding="utf-8") as fh:
|
|
|
|
|
|
contenu = json.load(fh)
|
|
|
|
|
|
except (OSError, ValueError):
|
|
|
|
|
|
continue
|
|
|
|
|
|
for plugin in contenu.get("plugins", []):
|
|
|
|
|
|
nom = plugin.get("name", "")
|
|
|
|
|
|
if nom:
|
|
|
|
|
|
catalogue.append(
|
|
|
|
|
|
(nom, nom_marche, plugin.get("description", ""))
|
|
|
|
|
|
)
|
|
|
|
|
|
return catalogue
|
|
|
|
|
|
|
|
|
|
|
|
def _claude_plugin_search(self):
|
|
|
|
|
|
"""Cherche un mot-clé dans le nom et la description des plugins."""
|
|
|
|
|
|
catalogue = self._claude_marketplace_catalog()
|
|
|
|
|
|
if not catalogue:
|
|
|
|
|
|
print(t("No marketplace is configured."))
|
|
|
|
|
|
return
|
|
|
|
|
|
mot = input(t("Keyword to search: ")).strip().lower()
|
|
|
|
|
|
if not mot:
|
|
|
|
|
|
print(t("Nothing to do."))
|
|
|
|
|
|
return
|
|
|
|
|
|
trouves = [
|
|
|
|
|
|
(nom, marche, desc)
|
|
|
|
|
|
for nom, marche, desc in catalogue
|
|
|
|
|
|
if mot in nom.lower() or mot in desc.lower()
|
|
|
|
|
|
]
|
|
|
|
|
|
if not trouves:
|
|
|
|
|
|
print(t("No plugin matches this keyword."))
|
|
|
|
|
|
return
|
|
|
|
|
|
print("-" * 78)
|
|
|
|
|
|
for nom, marche, desc in trouves:
|
|
|
|
|
|
print(f" {nom}@{marche}")
|
|
|
|
|
|
if desc:
|
|
|
|
|
|
print(f" {desc[:70]}")
|
|
|
|
|
|
print("-" * 78)
|
|
|
|
|
|
print(f"{t('Total:')} {len(trouves)}")
|
|
|
|
|
|
|
2025-10-31 01:10:54 -04:00
|
|
|
|
def prompt_execute_doc(self):
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(f"🤖 {t('Looking for documentation?')}")
|
2026-03-10 03:45:11 -04:00
|
|
|
|
choices = [
|
2026-03-12 05:31:45 -04:00
|
|
|
|
{"prompt_description": t("Migration module coverage")},
|
|
|
|
|
|
{"prompt_description": t("What change between version")},
|
|
|
|
|
|
{"prompt_description": t("OCA guidelines")},
|
|
|
|
|
|
{"prompt_description": t("OCA migration Odoo 19 milestone")},
|
2025-10-31 01:10:54 -04:00
|
|
|
|
]
|
2026-03-10 03:45:11 -04:00
|
|
|
|
help_info = self.fill_help_info(choices)
|
2025-10-31 01:10:54 -04:00
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
|
print()
|
|
|
|
|
|
if status == "0":
|
|
|
|
|
|
return False
|
|
|
|
|
|
elif status == "1":
|
|
|
|
|
|
str_version = input(
|
|
|
|
|
|
"Select version to upgrade Odoo CE (5-17) : "
|
|
|
|
|
|
)
|
|
|
|
|
|
try:
|
|
|
|
|
|
int_version = int(str_version)
|
|
|
|
|
|
print(
|
|
|
|
|
|
"https://oca.github.io/OpenUpgrade/coverage_analysis/modules"
|
|
|
|
|
|
f"{int_version * 10}-{(int_version + 1) * 10}.html"
|
|
|
|
|
|
)
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
print(
|
|
|
|
|
|
"https://oca.github.io/OpenUpgrade/030_coverage_analysis.html"
|
|
|
|
|
|
)
|
|
|
|
|
|
elif status == "2":
|
|
|
|
|
|
str_version = input(
|
|
|
|
|
|
"Select version to show what change for Odoo CE version 8-18) : "
|
|
|
|
|
|
)
|
|
|
|
|
|
try:
|
|
|
|
|
|
int_version = int(str_version)
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"https://github.com/OCA/maintainer-tools/wiki/Migration-to-version-{int_version}.0"
|
|
|
|
|
|
)
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
print("https://github.com/OCA/maintainer-tools/wiki")
|
|
|
|
|
|
elif status == "3":
|
|
|
|
|
|
print(
|
|
|
|
|
|
"https://github.com/OCA/odoo-community.org/blob/master/website/Contribution/CONTRIBUTING.rst"
|
|
|
|
|
|
)
|
|
|
|
|
|
elif status == "4":
|
|
|
|
|
|
print("https://github.com/OCA/maintainer-tools/issues/658")
|
|
|
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(t("Command not found !"))
|
2025-10-31 01:10:54 -04:00
|
|
|
|
|
|
|
|
|
|
def prompt_execute_database(self):
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(f"🤖 {t('Make changes to databases!')}")
|
2026-03-10 03:45:11 -04:00
|
|
|
|
choices = [
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
{"section": t("Backup")},
|
|
|
|
|
|
{"prompt_description": t("Create backup (.zip)")},
|
2026-03-13 15:04:14 -04:00
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
|
"Download database to create backup (.zip)"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
{"section": t("Restore")},
|
2026-03-12 05:31:45 -04:00
|
|
|
|
{"prompt_description": t("Restore from backup (.zip)")},
|
[ADD] database: dupliquer une base, et la neutraliser pour de bon
La duplication passe par exp_duplicate_database d'Odoo plutôt que par CREATE
DATABASE ... TEMPLATE : lui seul coupe les connexions de la source, régénère
database.uuid, copie le filestore et neutralise. Les trois modules maison du
dépôt n'obtenaient aucun des quatre.
Sous Odoo 15 et avant la neutralisation n'existe pas : la demande est REFUSÉE,
jamais ignorée — une copie qu'on croit neutralisée est pire qu'une brute.
Vérifié sur la copie d'une base migrée 12 → 18 : is_neutralized posé, crons
réduits au seul autovacuum, clé de paiement effacée, filestore copié.
--- EN ---
Duplication goes through Odoo's exp_duplicate_database rather than CREATE
DATABASE ... TEMPLATE: only it drops the source's connections, regenerates
database.uuid, copies the filestore and neutralises. The repository's three
in-house modules obtained none of the four.
Under Odoo 15 and earlier neutralisation does not exist: the request is
REFUSED, never ignored — a copy believed neutralised is worse than a raw one.
Checked on the copy of a 12 → 18 migrated database: is_neutralized set, crons
reduced to autovacuum alone, payment key cleared, filestore copied.
Assisted-by: Claude Opus 5
(cherry picked from commit af99d450c12e70a891d01d0ccf556c1d560e943b)
2026-08-25 03:55:46 -04:00
|
|
|
|
{"section": t("Duplicate")},
|
|
|
|
|
|
{"prompt_description": t("Duplicate a database")},
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
{"section": t("Danger zone")},
|
2026-06-25 02:25:40 -04:00
|
|
|
|
{"prompt_description": t("Erase a database")},
|
2025-10-31 01:10:54 -04:00
|
|
|
|
]
|
2026-03-10 03:45:11 -04:00
|
|
|
|
help_info = self.fill_help_info(choices)
|
2025-10-31 01:10:54 -04:00
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
|
print()
|
|
|
|
|
|
if status == "0":
|
|
|
|
|
|
return False
|
|
|
|
|
|
elif status == "1":
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
self.db_manager.create_backup_from_database()
|
2025-10-31 01:10:54 -04:00
|
|
|
|
elif status == "2":
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
self.db_manager.download_database_backup_cli()
|
2026-03-02 16:06:30 -05:00
|
|
|
|
elif status == "3":
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
self.db_manager.restore_from_database()
|
2026-06-25 02:25:40 -04:00
|
|
|
|
elif status == "4":
|
[ADD] database: dupliquer une base, et la neutraliser pour de bon
La duplication passe par exp_duplicate_database d'Odoo plutôt que par CREATE
DATABASE ... TEMPLATE : lui seul coupe les connexions de la source, régénère
database.uuid, copie le filestore et neutralise. Les trois modules maison du
dépôt n'obtenaient aucun des quatre.
Sous Odoo 15 et avant la neutralisation n'existe pas : la demande est REFUSÉE,
jamais ignorée — une copie qu'on croit neutralisée est pire qu'une brute.
Vérifié sur la copie d'une base migrée 12 → 18 : is_neutralized posé, crons
réduits au seul autovacuum, clé de paiement effacée, filestore copié.
--- EN ---
Duplication goes through Odoo's exp_duplicate_database rather than CREATE
DATABASE ... TEMPLATE: only it drops the source's connections, regenerates
database.uuid, copies the filestore and neutralises. The repository's three
in-house modules obtained none of the four.
Under Odoo 15 and earlier neutralisation does not exist: the request is
REFUSED, never ignored — a copy believed neutralised is worse than a raw one.
Checked on the copy of a 12 → 18 migrated database: is_neutralized set, crons
reduced to autovacuum alone, payment key cleared, filestore copied.
Assisted-by: Claude Opus 5
(cherry picked from commit af99d450c12e70a891d01d0ccf556c1d560e943b)
2026-08-25 03:55:46 -04:00
|
|
|
|
self.db_manager.duplicate_database()
|
|
|
|
|
|
elif status == "5":
|
2026-06-25 02:25:40 -04:00
|
|
|
|
self.db_manager.drop_database()
|
2025-10-31 01:10:54 -04:00
|
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(t("Command not found !"))
|
2025-10-31 01:10:54 -04:00
|
|
|
|
|
[ADD] analyse: inspect an Odoo database without restoring it
A read-only toolkit answering « what is in this database, and what will
break on a version bump »: schema size and tables no model claims,
customised views compared with their module source, and the x_ fields
Studio left behind. It reads a backup zip directly, so a 40 GB dump needs
no restore, and it reaches every answer from the TODO menu.
--- FR ---
Une boîte à outils en lecture seule qui répond à « qu'y a-t-il dans cette
base, et qu'est-ce qui cassera à la montée de version » : taille du schéma
et tables qu'aucun modèle ne réclame, vues personnalisées comparées à leur
source, champs x_ laissés par Studio. Elle lit un zip de sauvegarde tel
quel, donc sans restaurer, et tout s'atteint depuis le menu TODO.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
def prompt_execute_analyse(self):
|
[ADD] analyse: offer to install the suggested modules that are ready
After the report, only the "available" ones are offered — an unknown
module is not in the addons path and an uninstallable one has a broken
dependency, so listing them would buy three failures. How many were
left out is stated, else the count would look like a bug.
This is the only write in the Analyse menu, so its header no longer
claims otherwise. Three guards: the checkout must match the database
version (an Odoo 18 run against a 12 rewrites it before failing), both
questions default to no, and a rejected token is always shown.
--- FR ---
Après le rapport, seuls les « available » sont proposés — un module
inconnu n'est pas dans le chemin des addons, un cassé a une dépendance
morte : les lister achèterait trois échecs. Le nombre d'écartés est dit,
sinon l'écart de comptage passerait pour un bogue.
C'est la seule écriture du menu Analyse, dont l'en-tête ne prétend donc
plus le contraire. Trois garde-fous : le checkout doit être sur la
version de la base (un Odoo 18 lancé sur une 12 la réécrit avant
d'échouer), les deux questions valent non par défaut, et un jeton
refusé est toujours montré.
Assisted-by: Claude Opus 5
2026-08-21 04:31:36 -04:00
|
|
|
|
"""Analyses d'une base Odoo, en lecture seule.
|
[ADD] analyse: inspect an Odoo database without restoring it
A read-only toolkit answering « what is in this database, and what will
break on a version bump »: schema size and tables no model claims,
customised views compared with their module source, and the x_ fields
Studio left behind. It reads a backup zip directly, so a 40 GB dump needs
no restore, and it reaches every answer from the TODO menu.
--- FR ---
Une boîte à outils en lecture seule qui répond à « qu'y a-t-il dans cette
base, et qu'est-ce qui cassera à la montée de version » : taille du schéma
et tables qu'aucun modèle ne réclame, vues personnalisées comparées à leur
source, champs x_ laissés par Studio. Elle lit un zip de sauvegarde tel
quel, donc sans restaurer, et tout s'atteint depuis le menu TODO.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
|
[ADD] analyse: offer to install the suggested modules that are ready
After the report, only the "available" ones are offered — an unknown
module is not in the addons path and an uninstallable one has a broken
dependency, so listing them would buy three failures. How many were
left out is stated, else the count would look like a bug.
This is the only write in the Analyse menu, so its header no longer
claims otherwise. Three guards: the checkout must match the database
version (an Odoo 18 run against a 12 rewrites it before failing), both
questions default to no, and a rejected token is always shown.
--- FR ---
Après le rapport, seuls les « available » sont proposés — un module
inconnu n'est pas dans le chemin des addons, un cassé a une dépendance
morte : les lister achèterait trois échecs. Le nombre d'écartés est dit,
sinon l'écart de comptage passerait pour un bogue.
C'est la seule écriture du menu Analyse, dont l'en-tête ne prétend donc
plus le contraire. Trois garde-fous : le checkout doit être sur la
version de la base (un Odoo 18 lancé sur une 12 la réécrit avant
d'échouer), les deux questions valent non par défaut, et un jeton
refusé est toujours montré.
Assisted-by: Claude Opus 5
2026-08-21 04:31:36 -04:00
|
|
|
|
Toute LECTURE passe par une connexion psql ouverte avec
|
|
|
|
|
|
`default_transaction_read_only=on` : c'est le serveur qui refuse
|
|
|
|
|
|
l'écriture, pas une promesse du code.
|
|
|
|
|
|
|
|
|
|
|
|
Une seule action écrit — installer les modules suggérés, à la fin
|
|
|
|
|
|
de l'analyse [5]. Elle ne part jamais seule : question explicite,
|
|
|
|
|
|
défaut à « non », liste à confirmer, et refus net si le checkout
|
|
|
|
|
|
n'est pas sur la version de la base.
|
[ADD] analyse: inspect an Odoo database without restoring it
A read-only toolkit answering « what is in this database, and what will
break on a version bump »: schema size and tables no model claims,
customised views compared with their module source, and the x_ fields
Studio left behind. It reads a backup zip directly, so a 40 GB dump needs
no restore, and it reaches every answer from the TODO menu.
--- FR ---
Une boîte à outils en lecture seule qui répond à « qu'y a-t-il dans cette
base, et qu'est-ce qui cassera à la montée de version » : taille du schéma
et tables qu'aucun modèle ne réclame, vues personnalisées comparées à leur
source, champs x_ laissés par Studio. Elle lit un zip de sauvegarde tel
quel, donc sans restaurer, et tout s'atteint depuis le menu TODO.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
"""
|
[ADD] analyse: offer to install the suggested modules that are ready
After the report, only the "available" ones are offered — an unknown
module is not in the addons path and an uninstallable one has a broken
dependency, so listing them would buy three failures. How many were
left out is stated, else the count would look like a bug.
This is the only write in the Analyse menu, so its header no longer
claims otherwise. Three guards: the checkout must match the database
version (an Odoo 18 run against a 12 rewrites it before failing), both
questions default to no, and a rejected token is always shown.
--- FR ---
Après le rapport, seuls les « available » sont proposés — un module
inconnu n'est pas dans le chemin des addons, un cassé a une dépendance
morte : les lister achèterait trois échecs. Le nombre d'écartés est dit,
sinon l'écart de comptage passerait pour un bogue.
C'est la seule écriture du menu Analyse, dont l'en-tête ne prétend donc
plus le contraire. Trois garde-fous : le checkout doit être sur la
version de la base (un Odoo 18 lancé sur une 12 la réécrit avant
d'échouer), les deux questions valent non par défaut, et un jeton
refusé est toujours montré.
Assisted-by: Claude Opus 5
2026-08-21 04:31:36 -04:00
|
|
|
|
print(f"🤖 {t('Analyse a database. Reading never writes.')}")
|
[ADD] analyse: inspect an Odoo database without restoring it
A read-only toolkit answering « what is in this database, and what will
break on a version bump »: schema size and tables no model claims,
customised views compared with their module source, and the x_ fields
Studio left behind. It reads a backup zip directly, so a 40 GB dump needs
no restore, and it reaches every answer from the TODO menu.
--- FR ---
Une boîte à outils en lecture seule qui répond à « qu'y a-t-il dans cette
base, et qu'est-ce qui cassera à la montée de version » : taille du schéma
et tables qu'aucun modèle ne réclame, vues personnalisées comparées à leur
source, champs x_ laissés par Studio. Elle lit un zip de sauvegarde tel
quel, donc sans restaurer, et tout s'atteint depuis le menu TODO.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
choices = [
|
|
|
|
|
|
{"section": t("Structure")},
|
|
|
|
|
|
{"prompt_description": t("Tables and database size")},
|
|
|
|
|
|
{"section": t("Customisation")},
|
|
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
|
"Customised views, website copies included"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
|
|
|
|
|
{"prompt_description": t("Studio and hand-made x_ fields")},
|
[ADD] analyse: what a migration gained and lost, step by step
A migration leaves one database per step and they all still exist, so the
tool compares them side by side rather than replaying anything. Six Odoo
starts would cost an hour, write to the databases and need the checkout
switched at each step; the same inspection in SQL takes under half a
second per database and touches nothing. Measured on a real migration:
seven databases surveyed in 3.8 seconds.
It reports what each step gained and lost — modules, models, views, menus,
attachments — and ends on the comparison of the ENDS, which is not the sum
of the steps: a module dropped at 15 and put back at 17 lost nothing, and
adding the steps would count it twice.
The signal that matters is rows. A module removed is visible; a table
going from four thousand rows to zero is visible nowhere.
Two rename heuristics were tried and rejected on real data before the one
that holds. Row count alone paired an account tag table with dms_directory
— both had seven rows. A shared five-letter word paired two cleanup
tables. Overall name similarity separates them. And a renamed table stays
in the loss list, annotated: removing it was the real danger, since a
wrong pairing would have hidden a genuine loss.
--- FR ---
[ADD] analyse : ce qu'une migration a gagné et perdu, palier par palier
Une migration laisse une base par palier et elles existent toutes encore :
l'outil les compare côte à côte plutôt que de rejouer quoi que ce soit.
Six démarrages d'Odoo coûteraient une heure et écriraient dans les bases ;
la même inspection en SQL prend moins d'une demi-seconde par base. Mesuré
sur une vraie migration : sept bases inspectées en 3,8 secondes.
Le bilan final compare les EXTRÉMITÉS, ce qui n'est pas la somme des
paliers : un module retiré en 15 puis remis en 17 n'a rien perdu.
Le signal qui compte, ce sont les lignes. Un module en moins se voit ; une
table qui passe de quatre mille lignes à zéro ne se voit nulle part.
Deux rapprochements de renommage ont été essayés et rejetés sur des
données réelles. Et une table renommée reste dans la liste des pertes,
annotée : l'en retirer était le vrai danger.
Assisted-by: Claude Opus 5
2026-08-19 08:21:12 -04:00
|
|
|
|
{"section": t("Migration")},
|
|
|
|
|
|
{"prompt_description": t("Quality of a migration, step by step")},
|
[ADD] analyse: modules a database lacks vs the default package
image_db.py --check_addons_exist asks whether a module is on DISK.
Nothing asked whether a given DATABASE has it. After six migration
steps, that is the question: the 18 instance grown from 12 has only 4
of the 15 modules odoo18.0_base ships.
Five verdicts, not one "missing": an available module installs, an
unknown one needs the addons path repaired first, an uninstallable one
has a broken dependency no install will work around.
shortdesc is varchar up to 15 and jsonb after, and step databases of
both shapes appear in one session — so the column type is read, never
assumed.
--- FR ---
image_db.py --check_addons_exist demande si un module est sur le
DISQUE. Personne ne demandait si une BASE donnée l'a. Après six paliers,
c'est pourtant la question : l'instance 18 issue de la 12 n'a que 4 des
15 modules d'odoo18.0_base.
Cinq verdicts, pas un « manquant » : un module disponible s'installe, un
inconnu exige d'abord de réparer le chemin des addons, un cassé a une
dépendance qu'aucune installation ne contournera.
shortdesc est varchar jusqu'en 15 et jsonb ensuite, et les deux formes
se présentent dans la même session — le type est lu, jamais supposé.
Assisted-by: Claude Opus 5
2026-08-21 04:01:01 -04:00
|
|
|
|
{"section": t("Modules")},
|
|
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
|
"Modules missing from the default package"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
[ADD] analyse: qui dépend de qui, à l'écran
Le menu Analyse disait quels modules manquent, jamais qui dépend de qui.
« Puis-je retirer celui-ci » se réglait donc à la main : au palier
17 → 18, il a fallu écrire la requête pour savoir si web_responsive
pouvait partir.
L'écran liste les modules et « d » parcourt quatre relations : ce dont il
dépend, ce qui en dépend, tout ce qu'il entraîne, tout ce qui tombe avec
lui. « f » filtre, « / » cherche — une base en porte trois mille. La
réponse qui compte est écrite en toutes lettres : deux dépendants
déclarés dont zéro installé, c'est un retrait sans danger.
--- EN ---
The Analyse menu told which modules were missing, never which depends on
which. « Can I remove this one » was therefore answered by hand: at the
17 → 18 step we had to write the query to learn whether web_responsive
could go.
The screen lists the modules and « d » walks four relations: what it
needs, what needs it, everything it pulls in, everything that falls with
it. « f » filters, « / » searches — a database holds three thousand of
them. The answer that matters is spelled out: two declared dependents of
which zero installed means removal is safe.
Assisted-by: Claude Opus 5
2026-08-23 03:46:08 -04:00
|
|
|
|
{"prompt_description": t("Dependencies between modules")},
|
[ADD] db_restore: check the filestore landed, and open the tool from the menu
Odoo's shutil.move renames when the destination is absent and NESTS when
it exists, so a leftover filestore/<db>/ sends a whole backup into
filestore/<db>/filestore/, where Odoo never looks. That happened once
here and the clone copied it into all seven databases of the chain --
1168 files, 133 MB each, and nothing said a word.
The check runs after a real restore only. A clone copies its source as
it stands, faults included: checking the mirror would say the same thing
twice, and in the wrong place. It warns and names the fix rather than
aborting -- the database is restored and usable, it is the layout that
is wrong.
--- FR ---
Le shutil.move d'Odoo renomme quand la destination est absente et
IMBRIQUE quand elle existe : un filestore/<base>/ resté là envoie toute
une sauvegarde dans filestore/<base>/filestore/, où Odoo ne regarde
jamais. C'est arrivé une fois ici et le clone l'a recopié dans les sept
bases de la chaîne -- 1168 fichiers, 133 Mo chacune, sans un mot.
Le contrôle ne suit qu'une vraie restauration. Un clone recopie sa
source telle quelle, défauts compris : contrôler le miroir dirait deux
fois la même chose, au mauvais endroit. Il avertit et nomme la
correction plutôt que d'interrompre -- la base est restaurée et
utilisable, c'est la disposition qui cloche.
Assisted-by: Claude Opus 5
2026-08-21 22:14:48 -04:00
|
|
|
|
{"section": t("Files")},
|
|
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
|
"Attachment files missing from the filestore"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
[ADD] analyse: ausculter une base qui n'est pas ici
Les analyses existaient ; le chemin d'AVANT manquait. La base d'un client
est dans un zip, derrière une URL, ou vivante sur un serveur.
« Restant de migration » a dû être écrit : check_migration_quality compare
les bases de PALIER et exige le journal de progression — devant une
sauvegarde isolée, ni l'un ni l'autre n'existe.
Ses compteurs évidents ont été écartés après mesure. Comparés à la base
d'ORIGINE : champs sans colonne 25 → 72, modèles sans table 90 → 158.
Vingt-cinq et quatre-vingt-dix AVANT toute migration : du bruit. Ne
restent que les constats faux en eux-mêmes, 0 avant, non nuls après —
9 langues au drapeau NULL, 68 tables m2m absentes, 414 index doublés.
Le passe-plat RPC n'accepte que la lecture. psql l'obtient du serveur ;
une session RPC n'a rien d'équivalent, et la liste blanche est donc
appliquée dans le passe-plat, pas chez l'appelant.
--- EN ---
The analyses existed; the path BEFORE them did not. A customer database
sits in a zip, behind a URL, or live on a server.
« Migration leftovers » had to be written: check_migration_quality
compares STEP databases and needs the progression log — facing a lone
backup, neither exists.
Its obvious counters were dropped after measuring. Against the ORIGINAL
database: fields with no column 25 → 72, models with no table 90 → 158.
Twenty-five and ninety BEFORE any migration: noise. Only what is wrong in
itself remains, 0 before and non-zero after — 9 languages with a NULL
flag, 68 missing m2m tables, 414 duplicated indexes.
The RPC proxy only reads. psql gets that from the server; an RPC session
has no equivalent, so the allowlist lives in the proxy, not the caller.
Assisted-by: Claude Opus 5
2026-08-24 07:21:37 -04:00
|
|
|
|
{"section": t("Instance")},
|
|
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
|
"Monitoring - a backup, a remote copy or a live instance"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
[ADD] analyse: inspect an Odoo database without restoring it
A read-only toolkit answering « what is in this database, and what will
break on a version bump »: schema size and tables no model claims,
customised views compared with their module source, and the x_ fields
Studio left behind. It reads a backup zip directly, so a 40 GB dump needs
no restore, and it reaches every answer from the TODO menu.
--- FR ---
Une boîte à outils en lecture seule qui répond à « qu'y a-t-il dans cette
base, et qu'est-ce qui cassera à la montée de version » : taille du schéma
et tables qu'aucun modèle ne réclame, vues personnalisées comparées à leur
source, champs x_ laissés par Studio. Elle lit un zip de sauvegarde tel
quel, donc sans restaurer, et tout s'atteint depuis le menu TODO.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
]
|
|
|
|
|
|
help_info = self.fill_help_info(choices)
|
|
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
|
print()
|
|
|
|
|
|
if status == "0":
|
|
|
|
|
|
return False
|
|
|
|
|
|
elif status == "1":
|
|
|
|
|
|
self.execute_analyse_schema_size()
|
|
|
|
|
|
elif status == "2":
|
|
|
|
|
|
self.execute_analyse_view_custom()
|
|
|
|
|
|
elif status == "3":
|
|
|
|
|
|
self.execute_analyse_custom_field()
|
[ADD] analyse: what a migration gained and lost, step by step
A migration leaves one database per step and they all still exist, so the
tool compares them side by side rather than replaying anything. Six Odoo
starts would cost an hour, write to the databases and need the checkout
switched at each step; the same inspection in SQL takes under half a
second per database and touches nothing. Measured on a real migration:
seven databases surveyed in 3.8 seconds.
It reports what each step gained and lost — modules, models, views, menus,
attachments — and ends on the comparison of the ENDS, which is not the sum
of the steps: a module dropped at 15 and put back at 17 lost nothing, and
adding the steps would count it twice.
The signal that matters is rows. A module removed is visible; a table
going from four thousand rows to zero is visible nowhere.
Two rename heuristics were tried and rejected on real data before the one
that holds. Row count alone paired an account tag table with dms_directory
— both had seven rows. A shared five-letter word paired two cleanup
tables. Overall name similarity separates them. And a renamed table stays
in the loss list, annotated: removing it was the real danger, since a
wrong pairing would have hidden a genuine loss.
--- FR ---
[ADD] analyse : ce qu'une migration a gagné et perdu, palier par palier
Une migration laisse une base par palier et elles existent toutes encore :
l'outil les compare côte à côte plutôt que de rejouer quoi que ce soit.
Six démarrages d'Odoo coûteraient une heure et écriraient dans les bases ;
la même inspection en SQL prend moins d'une demi-seconde par base. Mesuré
sur une vraie migration : sept bases inspectées en 3,8 secondes.
Le bilan final compare les EXTRÉMITÉS, ce qui n'est pas la somme des
paliers : un module retiré en 15 puis remis en 17 n'a rien perdu.
Le signal qui compte, ce sont les lignes. Un module en moins se voit ; une
table qui passe de quatre mille lignes à zéro ne se voit nulle part.
Deux rapprochements de renommage ont été essayés et rejetés sur des
données réelles. Et une table renommée reste dans la liste des pertes,
annotée : l'en retirer était le vrai danger.
Assisted-by: Claude Opus 5
2026-08-19 08:21:12 -04:00
|
|
|
|
elif status == "4":
|
|
|
|
|
|
self.execute_analyse_migration_quality()
|
[ADD] analyse: modules a database lacks vs the default package
image_db.py --check_addons_exist asks whether a module is on DISK.
Nothing asked whether a given DATABASE has it. After six migration
steps, that is the question: the 18 instance grown from 12 has only 4
of the 15 modules odoo18.0_base ships.
Five verdicts, not one "missing": an available module installs, an
unknown one needs the addons path repaired first, an uninstallable one
has a broken dependency no install will work around.
shortdesc is varchar up to 15 and jsonb after, and step databases of
both shapes appear in one session — so the column type is read, never
assumed.
--- FR ---
image_db.py --check_addons_exist demande si un module est sur le
DISQUE. Personne ne demandait si une BASE donnée l'a. Après six paliers,
c'est pourtant la question : l'instance 18 issue de la 12 n'a que 4 des
15 modules d'odoo18.0_base.
Cinq verdicts, pas un « manquant » : un module disponible s'installe, un
inconnu exige d'abord de réparer le chemin des addons, un cassé a une
dépendance qu'aucune installation ne contournera.
shortdesc est varchar jusqu'en 15 et jsonb ensuite, et les deux formes
se présentent dans la même session — le type est lu, jamais supposé.
Assisted-by: Claude Opus 5
2026-08-21 04:01:01 -04:00
|
|
|
|
elif status == "5":
|
|
|
|
|
|
self.execute_analyse_module_package()
|
[ADD] db_restore: check the filestore landed, and open the tool from the menu
Odoo's shutil.move renames when the destination is absent and NESTS when
it exists, so a leftover filestore/<db>/ sends a whole backup into
filestore/<db>/filestore/, where Odoo never looks. That happened once
here and the clone copied it into all seven databases of the chain --
1168 files, 133 MB each, and nothing said a word.
The check runs after a real restore only. A clone copies its source as
it stands, faults included: checking the mirror would say the same thing
twice, and in the wrong place. It warns and names the fix rather than
aborting -- the database is restored and usable, it is the layout that
is wrong.
--- FR ---
Le shutil.move d'Odoo renomme quand la destination est absente et
IMBRIQUE quand elle existe : un filestore/<base>/ resté là envoie toute
une sauvegarde dans filestore/<base>/filestore/, où Odoo ne regarde
jamais. C'est arrivé une fois ici et le clone l'a recopié dans les sept
bases de la chaîne -- 1168 fichiers, 133 Mo chacune, sans un mot.
Le contrôle ne suit qu'une vraie restauration. Un clone recopie sa
source telle quelle, défauts compris : contrôler le miroir dirait deux
fois la même chose, au mauvais endroit. Il avertit et nomme la
correction plutôt que d'interrompre -- la base est restaurée et
utilisable, c'est la disposition qui cloche.
Assisted-by: Claude Opus 5
2026-08-21 22:14:48 -04:00
|
|
|
|
elif status == "6":
|
[ADD] analyse: qui dépend de qui, à l'écran
Le menu Analyse disait quels modules manquent, jamais qui dépend de qui.
« Puis-je retirer celui-ci » se réglait donc à la main : au palier
17 → 18, il a fallu écrire la requête pour savoir si web_responsive
pouvait partir.
L'écran liste les modules et « d » parcourt quatre relations : ce dont il
dépend, ce qui en dépend, tout ce qu'il entraîne, tout ce qui tombe avec
lui. « f » filtre, « / » cherche — une base en porte trois mille. La
réponse qui compte est écrite en toutes lettres : deux dépendants
déclarés dont zéro installé, c'est un retrait sans danger.
--- EN ---
The Analyse menu told which modules were missing, never which depends on
which. « Can I remove this one » was therefore answered by hand: at the
17 → 18 step we had to write the query to learn whether web_responsive
could go.
The screen lists the modules and « d » walks four relations: what it
needs, what needs it, everything it pulls in, everything that falls with
it. « f » filters, « / » searches — a database holds three thousand of
them. The answer that matters is spelled out: two declared dependents of
which zero installed means removal is safe.
Assisted-by: Claude Opus 5
2026-08-23 03:46:08 -04:00
|
|
|
|
self.execute_analyse_module_dependency()
|
|
|
|
|
|
elif status == "7":
|
[ADD] db_restore: check the filestore landed, and open the tool from the menu
Odoo's shutil.move renames when the destination is absent and NESTS when
it exists, so a leftover filestore/<db>/ sends a whole backup into
filestore/<db>/filestore/, where Odoo never looks. That happened once
here and the clone copied it into all seven databases of the chain --
1168 files, 133 MB each, and nothing said a word.
The check runs after a real restore only. A clone copies its source as
it stands, faults included: checking the mirror would say the same thing
twice, and in the wrong place. It warns and names the fix rather than
aborting -- the database is restored and usable, it is the layout that
is wrong.
--- FR ---
Le shutil.move d'Odoo renomme quand la destination est absente et
IMBRIQUE quand elle existe : un filestore/<base>/ resté là envoie toute
une sauvegarde dans filestore/<base>/filestore/, où Odoo ne regarde
jamais. C'est arrivé une fois ici et le clone l'a recopié dans les sept
bases de la chaîne -- 1168 fichiers, 133 Mo chacune, sans un mot.
Le contrôle ne suit qu'une vraie restauration. Un clone recopie sa
source telle quelle, défauts compris : contrôler le miroir dirait deux
fois la même chose, au mauvais endroit. Il avertit et nomme la
correction plutôt que d'interrompre -- la base est restaurée et
utilisable, c'est la disposition qui cloche.
Assisted-by: Claude Opus 5
2026-08-21 22:14:48 -04:00
|
|
|
|
self.execute_analyse_filestore()
|
[ADD] analyse: ausculter une base qui n'est pas ici
Les analyses existaient ; le chemin d'AVANT manquait. La base d'un client
est dans un zip, derrière une URL, ou vivante sur un serveur.
« Restant de migration » a dû être écrit : check_migration_quality compare
les bases de PALIER et exige le journal de progression — devant une
sauvegarde isolée, ni l'un ni l'autre n'existe.
Ses compteurs évidents ont été écartés après mesure. Comparés à la base
d'ORIGINE : champs sans colonne 25 → 72, modèles sans table 90 → 158.
Vingt-cinq et quatre-vingt-dix AVANT toute migration : du bruit. Ne
restent que les constats faux en eux-mêmes, 0 avant, non nuls après —
9 langues au drapeau NULL, 68 tables m2m absentes, 414 index doublés.
Le passe-plat RPC n'accepte que la lecture. psql l'obtient du serveur ;
une session RPC n'a rien d'équivalent, et la liste blanche est donc
appliquée dans le passe-plat, pas chez l'appelant.
--- EN ---
The analyses existed; the path BEFORE them did not. A customer database
sits in a zip, behind a URL, or live on a server.
« Migration leftovers » had to be written: check_migration_quality
compares STEP databases and needs the progression log — facing a lone
backup, neither exists.
Its obvious counters were dropped after measuring. Against the ORIGINAL
database: fields with no column 25 → 72, models with no table 90 → 158.
Twenty-five and ninety BEFORE any migration: noise. Only what is wrong in
itself remains, 0 before and non-zero after — 9 languages with a NULL
flag, 68 missing m2m tables, 414 duplicated indexes.
The RPC proxy only reads. psql gets that from the server; an RPC session
has no equivalent, so the allowlist lives in the proxy, not the caller.
Assisted-by: Claude Opus 5
2026-08-24 07:21:37 -04:00
|
|
|
|
elif status == "8":
|
|
|
|
|
|
self.execute_analyse_monitoring()
|
[ADD] analyse: inspect an Odoo database without restoring it
A read-only toolkit answering « what is in this database, and what will
break on a version bump »: schema size and tables no model claims,
customised views compared with their module source, and the x_ fields
Studio left behind. It reads a backup zip directly, so a 40 GB dump needs
no restore, and it reaches every answer from the TODO menu.
--- FR ---
Une boîte à outils en lecture seule qui répond à « qu'y a-t-il dans cette
base, et qu'est-ce qui cassera à la montée de version » : taille du schéma
et tables qu'aucun modèle ne réclame, vues personnalisées comparées à leur
source, champs x_ laissés par Studio. Elle lit un zip de sauvegarde tel
quel, donc sans restaurer, et tout s'atteint depuis le menu TODO.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
else:
|
|
|
|
|
|
print(t("Command not found !"))
|
|
|
|
|
|
|
[ADD] analyse: modules a database lacks vs the default package
image_db.py --check_addons_exist asks whether a module is on DISK.
Nothing asked whether a given DATABASE has it. After six migration
steps, that is the question: the 18 instance grown from 12 has only 4
of the 15 modules odoo18.0_base ships.
Five verdicts, not one "missing": an available module installs, an
unknown one needs the addons path repaired first, an uninstallable one
has a broken dependency no install will work around.
shortdesc is varchar up to 15 and jsonb after, and step databases of
both shapes appear in one session — so the column type is read, never
assumed.
--- FR ---
image_db.py --check_addons_exist demande si un module est sur le
DISQUE. Personne ne demandait si une BASE donnée l'a. Après six paliers,
c'est pourtant la question : l'instance 18 issue de la 12 n'a que 4 des
15 modules d'odoo18.0_base.
Cinq verdicts, pas un « manquant » : un module disponible s'installe, un
inconnu exige d'abord de réparer le chemin des addons, un cassé a une
dépendance qu'aucune installation ne contournera.
shortdesc est varchar jusqu'en 15 et jsonb ensuite, et les deux formes
se présentent dans la même session — le type est lu, jamais supposé.
Assisted-by: Claude Opus 5
2026-08-21 04:01:01 -04:00
|
|
|
|
def execute_analyse_module_package(self):
|
|
|
|
|
|
"""Ce que la base n'a pas, alors que l'installation par défaut l'a.
|
|
|
|
|
|
|
|
|
|
|
|
Pas de choix « sauvegarde .zip » ici, contrairement aux autres
|
|
|
|
|
|
analyses : l'outil interroge `ir_module_module`, qu'un zip
|
|
|
|
|
|
n'expose pas sans restauration. Proposer l'option pour la refuser
|
|
|
|
|
|
ensuite ferait perdre le temps de la choisir.
|
|
|
|
|
|
"""
|
|
|
|
|
|
from script.analyse import check_module_package as modules
|
|
|
|
|
|
|
|
|
|
|
|
database = self._analyse_select_database()
|
|
|
|
|
|
if not database:
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
rapport = modules.audit(database)
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
print(f"❌ {t('Analysis failed: ')}{exc}")
|
|
|
|
|
|
return
|
|
|
|
|
|
if rapport.get("unavailable"):
|
|
|
|
|
|
print(f"❌ {t('Cannot read the database: ')}{database}")
|
|
|
|
|
|
return
|
|
|
|
|
|
print("\n".join(modules.render(rapport, limit=8)))
|
[ADD] analyse: offer to install the suggested modules that are ready
After the report, only the "available" ones are offered — an unknown
module is not in the addons path and an uninstallable one has a broken
dependency, so listing them would buy three failures. How many were
left out is stated, else the count would look like a bug.
This is the only write in the Analyse menu, so its header no longer
claims otherwise. Three guards: the checkout must match the database
version (an Odoo 18 run against a 12 rewrites it before failing), both
questions default to no, and a rejected token is always shown.
--- FR ---
Après le rapport, seuls les « available » sont proposés — un module
inconnu n'est pas dans le chemin des addons, un cassé a une dépendance
morte : les lister achèterait trois échecs. Le nombre d'écartés est dit,
sinon l'écart de comptage passerait pour un bogue.
C'est la seule écriture du menu Analyse, dont l'en-tête ne prétend donc
plus le contraire. Trois garde-fous : le checkout doit être sur la
version de la base (un Odoo 18 lancé sur une 12 la réécrit avant
d'échouer), les deux questions valent non par défaut, et un jeton
refusé est toujours montré.
Assisted-by: Claude Opus 5
2026-08-21 04:31:36 -04:00
|
|
|
|
self._analyse_offer_install(database, rapport)
|
[ADD] analyse: modules a database lacks vs the default package
image_db.py --check_addons_exist asks whether a module is on DISK.
Nothing asked whether a given DATABASE has it. After six migration
steps, that is the question: the 18 instance grown from 12 has only 4
of the 15 modules odoo18.0_base ships.
Five verdicts, not one "missing": an available module installs, an
unknown one needs the addons path repaired first, an uninstallable one
has a broken dependency no install will work around.
shortdesc is varchar up to 15 and jsonb after, and step databases of
both shapes appear in one session — so the column type is read, never
assumed.
--- FR ---
image_db.py --check_addons_exist demande si un module est sur le
DISQUE. Personne ne demandait si une BASE donnée l'a. Après six paliers,
c'est pourtant la question : l'instance 18 issue de la 12 n'a que 4 des
15 modules d'odoo18.0_base.
Cinq verdicts, pas un « manquant » : un module disponible s'installe, un
inconnu exige d'abord de réparer le chemin des addons, un cassé a une
dépendance qu'aucune installation ne contournera.
shortdesc est varchar jusqu'en 15 et jsonb ensuite, et les deux formes
se présentent dans la même session — le type est lu, jamais supposé.
Assisted-by: Claude Opus 5
2026-08-21 04:01:01 -04:00
|
|
|
|
|
|
|
|
|
|
def handler(rank):
|
|
|
|
|
|
if rank == 1:
|
|
|
|
|
|
print("\n".join(modules.render(rapport, limit=0)))
|
|
|
|
|
|
elif rank == 2:
|
|
|
|
|
|
for nom in sorted(modules.read_packages()):
|
|
|
|
|
|
print(f" {nom}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
self._analyse_export_json(
|
|
|
|
|
|
rapport, os.path.basename(database), "module_package"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
self._analyse_follow_up(
|
|
|
|
|
|
[
|
|
|
|
|
|
{"prompt_description": t("Show every entry")},
|
|
|
|
|
|
{"prompt_description": t("List the known packages")},
|
|
|
|
|
|
{"prompt_description": t("Export as JSON")},
|
|
|
|
|
|
],
|
|
|
|
|
|
handler,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
[ADD] analyse: qui dépend de qui, à l'écran
Le menu Analyse disait quels modules manquent, jamais qui dépend de qui.
« Puis-je retirer celui-ci » se réglait donc à la main : au palier
17 → 18, il a fallu écrire la requête pour savoir si web_responsive
pouvait partir.
L'écran liste les modules et « d » parcourt quatre relations : ce dont il
dépend, ce qui en dépend, tout ce qu'il entraîne, tout ce qui tombe avec
lui. « f » filtre, « / » cherche — une base en porte trois mille. La
réponse qui compte est écrite en toutes lettres : deux dépendants
déclarés dont zéro installé, c'est un retrait sans danger.
--- EN ---
The Analyse menu told which modules were missing, never which depends on
which. « Can I remove this one » was therefore answered by hand: at the
17 → 18 step we had to write the query to learn whether web_responsive
could go.
The screen lists the modules and « d » walks four relations: what it
needs, what needs it, everything it pulls in, everything that falls with
it. « f » filters, « / » searches — a database holds three thousand of
them. The answer that matters is spelled out: two declared dependents of
which zero installed means removal is safe.
Assisted-by: Claude Opus 5
2026-08-23 03:46:08 -04:00
|
|
|
|
def execute_analyse_module_dependency(self):
|
|
|
|
|
|
"""Qui dépend de qui, pour savoir ce qu'on peut retirer.
|
|
|
|
|
|
|
|
|
|
|
|
L'écran est ouvert par l'outil lui-même, qui retombe sur son
|
|
|
|
|
|
rapport texte s'il ne peut pas — terminal absent, Textual absent.
|
|
|
|
|
|
|
|
|
|
|
|
Pas d'option « sauvegarde .zip » : les dépendances vivent dans
|
|
|
|
|
|
`ir_module_module_dependency`, qu'un zip n'expose pas sans
|
|
|
|
|
|
restauration.
|
|
|
|
|
|
"""
|
|
|
|
|
|
from script.analyse import check_module_dependency as dependency
|
|
|
|
|
|
|
|
|
|
|
|
database = self._analyse_select_database()
|
|
|
|
|
|
if not database:
|
|
|
|
|
|
return
|
|
|
|
|
|
print(f"⧖ {t('Reading the modules and their dependencies…')}")
|
|
|
|
|
|
try:
|
|
|
|
|
|
rapport = dependency.survey(database)
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
print(f"❌ {t('Analysis failed: ')}{exc}")
|
|
|
|
|
|
return
|
|
|
|
|
|
if rapport.get("unavailable"):
|
|
|
|
|
|
print(f"❌ {t('Cannot read the database: ')}{database}")
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
from script.analyse.check_module_dependency_tui import run_tui
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
run_tui = None
|
|
|
|
|
|
if not (run_tui and run_tui(rapport)):
|
|
|
|
|
|
# Borné : une base porte trois mille modules, et déverser six
|
|
|
|
|
|
# mille lignes dans le menu n'est pas un repli.
|
|
|
|
|
|
print("\n".join(dependency.render_text(rapport, limit=8, cap=40)))
|
|
|
|
|
|
|
|
|
|
|
|
def handler(rank):
|
|
|
|
|
|
if rank == 1:
|
|
|
|
|
|
print("\n".join(dependency.render_text(rapport, limit=0)))
|
|
|
|
|
|
else:
|
|
|
|
|
|
self._analyse_export_json(
|
|
|
|
|
|
rapport, os.path.basename(database), "module_dependency"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
self._analyse_follow_up(
|
|
|
|
|
|
[
|
|
|
|
|
|
{"prompt_description": t("Show every entry")},
|
|
|
|
|
|
{"prompt_description": t("Export as JSON")},
|
|
|
|
|
|
],
|
|
|
|
|
|
handler,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
[ADD] db_restore: check the filestore landed, and open the tool from the menu
Odoo's shutil.move renames when the destination is absent and NESTS when
it exists, so a leftover filestore/<db>/ sends a whole backup into
filestore/<db>/filestore/, where Odoo never looks. That happened once
here and the clone copied it into all seven databases of the chain --
1168 files, 133 MB each, and nothing said a word.
The check runs after a real restore only. A clone copies its source as
it stands, faults included: checking the mirror would say the same thing
twice, and in the wrong place. It warns and names the fix rather than
aborting -- the database is restored and usable, it is the layout that
is wrong.
--- FR ---
Le shutil.move d'Odoo renomme quand la destination est absente et
IMBRIQUE quand elle existe : un filestore/<base>/ resté là envoie toute
une sauvegarde dans filestore/<base>/filestore/, où Odoo ne regarde
jamais. C'est arrivé une fois ici et le clone l'a recopié dans les sept
bases de la chaîne -- 1168 fichiers, 133 Mo chacune, sans un mot.
Le contrôle ne suit qu'une vraie restauration. Un clone recopie sa
source telle quelle, défauts compris : contrôler le miroir dirait deux
fois la même chose, au mauvais endroit. Il avertit et nomme la
correction plutôt que d'interrompre -- la base est restaurée et
utilisable, c'est la disposition qui cloche.
Assisted-by: Claude Opus 5
2026-08-21 22:14:48 -04:00
|
|
|
|
def execute_analyse_filestore(self):
|
|
|
|
|
|
"""Ce qui manque au filestore, et ce qu'on peut encore récupérer.
|
|
|
|
|
|
|
|
|
|
|
|
Pas d'option « sauvegarde .zip » : l'outil compare une BASE à son
|
|
|
|
|
|
filestore, et un zip porte les deux ensemble par construction —
|
|
|
|
|
|
il n'y a rien à y trouver.
|
|
|
|
|
|
"""
|
|
|
|
|
|
from script.analyse import check_filestore as filestore
|
|
|
|
|
|
|
|
|
|
|
|
database = self._analyse_select_database()
|
|
|
|
|
|
if not database:
|
|
|
|
|
|
return
|
|
|
|
|
|
print(f"⧖ {t('Scanning filestores and backups…')}")
|
|
|
|
|
|
try:
|
|
|
|
|
|
rapport = filestore.audit(database)
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
print(f"❌ {t('Analysis failed: ')}{exc}")
|
|
|
|
|
|
return
|
|
|
|
|
|
if rapport.get("unavailable"):
|
|
|
|
|
|
print(f"❌ {t('Cannot read the database: ')}{database}")
|
|
|
|
|
|
return
|
[FIX] filestore: five defects a real repair session brought out
Deduplicating by file was right for COUNTING and wrong for DELETING:
twenty-two rows shared two files, so the purge only ever offered one at
a time and had to be replayed. It now gathers every row whose own field
is dead, and never a live row sharing the same file.
"root" held the root of all filestores instead of this database's
directory, so tidying looked for a nested folder at
<data_dir>/filestore/filestore and answered "nothing to tidy" in front
of 1168 stranded files. It now finds 112 to move up and 1056 duplicates.
limit=0 means "no cap" everywhere else, but the slice [:0] is empty:
"Show every entry" printed "… 3 more" and showed none of them.
The report was captured once, so after a purge it replayed the state
from before -- one then purged rows already gone, believing the work
unfinished. A repair now says whether it did something, and the report
is re-read when it did.
"DELETE 0" was announced as a success. The count now comes from what
PostgreSQL said, and saying nothing is not the same as deleting nothing.
--- FR ---
Dédupliquer par fichier était juste pour COMPTER et faux pour EFFACER :
vingt-deux lignes partageaient deux fichiers, la purge n'en offrait
qu'une à la fois. Elle prend maintenant toutes les lignes dont le champ
est mort, et jamais une ligne vivante partageant le même fichier.
« root » portait la racine de tous les filestores au lieu du dossier de
la base : le rangement cherchait à <data_dir>/filestore/filestore et
répondait « rien à ranger » devant 1168 fichiers échoués. Il en trouve
112 à remonter et 1056 doublons.
limit=0 veut dire « tout » partout ailleurs, mais [:0] est vide : « Tout
afficher » annonçait « … 3 de plus » sans en montrer un seul.
Le rapport n'était lu qu'une fois : après une purge il rejouait l'état
d'avant, et l'on repurgeait des lignes déjà effacées. Une réparation dit
maintenant si elle a fait quelque chose, et le rapport est relu alors.
« DELETE 0 » passait pour un succès. Le compte vient de ce que
PostgreSQL a annoncé, et ne rien dire n'est pas ne rien supprimer.
Assisted-by: Claude Opus 5
2026-08-21 23:20:08 -04:00
|
|
|
|
etat = {"rapport": rapport}
|
[ADD] db_restore: check the filestore landed, and open the tool from the menu
Odoo's shutil.move renames when the destination is absent and NESTS when
it exists, so a leftover filestore/<db>/ sends a whole backup into
filestore/<db>/filestore/, where Odoo never looks. That happened once
here and the clone copied it into all seven databases of the chain --
1168 files, 133 MB each, and nothing said a word.
The check runs after a real restore only. A clone copies its source as
it stands, faults included: checking the mirror would say the same thing
twice, and in the wrong place. It warns and names the fix rather than
aborting -- the database is restored and usable, it is the layout that
is wrong.
--- FR ---
Le shutil.move d'Odoo renomme quand la destination est absente et
IMBRIQUE quand elle existe : un filestore/<base>/ resté là envoie toute
une sauvegarde dans filestore/<base>/filestore/, où Odoo ne regarde
jamais. C'est arrivé une fois ici et le clone l'a recopié dans les sept
bases de la chaîne -- 1168 fichiers, 133 Mo chacune, sans un mot.
Le contrôle ne suit qu'une vraie restauration. Un clone recopie sa
source telle quelle, défauts compris : contrôler le miroir dirait deux
fois la même chose, au mauvais endroit. Il avertit et nomme la
correction plutôt que d'interrompre -- la base est restaurée et
utilisable, c'est la disposition qui cloche.
Assisted-by: Claude Opus 5
2026-08-21 22:14:48 -04:00
|
|
|
|
print("\n".join(filestore.render(rapport, limit=20)))
|
|
|
|
|
|
|
[FIX] filestore: five defects a real repair session brought out
Deduplicating by file was right for COUNTING and wrong for DELETING:
twenty-two rows shared two files, so the purge only ever offered one at
a time and had to be replayed. It now gathers every row whose own field
is dead, and never a live row sharing the same file.
"root" held the root of all filestores instead of this database's
directory, so tidying looked for a nested folder at
<data_dir>/filestore/filestore and answered "nothing to tidy" in front
of 1168 stranded files. It now finds 112 to move up and 1056 duplicates.
limit=0 means "no cap" everywhere else, but the slice [:0] is empty:
"Show every entry" printed "… 3 more" and showed none of them.
The report was captured once, so after a purge it replayed the state
from before -- one then purged rows already gone, believing the work
unfinished. A repair now says whether it did something, and the report
is re-read when it did.
"DELETE 0" was announced as a success. The count now comes from what
PostgreSQL said, and saying nothing is not the same as deleting nothing.
--- FR ---
Dédupliquer par fichier était juste pour COMPTER et faux pour EFFACER :
vingt-deux lignes partageaient deux fichiers, la purge n'en offrait
qu'une à la fois. Elle prend maintenant toutes les lignes dont le champ
est mort, et jamais une ligne vivante partageant le même fichier.
« root » portait la racine de tous les filestores au lieu du dossier de
la base : le rangement cherchait à <data_dir>/filestore/filestore et
répondait « rien à ranger » devant 1168 fichiers échoués. Il en trouve
112 à remonter et 1056 doublons.
limit=0 veut dire « tout » partout ailleurs, mais [:0] est vide : « Tout
afficher » annonçait « … 3 de plus » sans en montrer un seul.
Le rapport n'était lu qu'une fois : après une purge il rejouait l'état
d'avant, et l'on repurgeait des lignes déjà effacées. Une réparation dit
maintenant si elle a fait quelque chose, et le rapport est relu alors.
« DELETE 0 » passait pour un succès. Le compte vient de ce que
PostgreSQL a annoncé, et ne rien dire n'est pas ne rien supprimer.
Assisted-by: Claude Opus 5
2026-08-21 23:20:08 -04:00
|
|
|
|
def relire():
|
|
|
|
|
|
"""Relire APRÈS une réparation.
|
|
|
|
|
|
|
|
|
|
|
|
Sans cela « Tout afficher » rejouait le rapport d'avant :
|
|
|
|
|
|
on purgeait, on relisait, et l'on voyait encore ce qui
|
|
|
|
|
|
venait de disparaître. Pire, on repurgeait des lignes déjà
|
|
|
|
|
|
effacées en croyant le travail inachevé.
|
|
|
|
|
|
"""
|
|
|
|
|
|
print(f"⧖ {t('Scanning filestores and backups…')}")
|
|
|
|
|
|
etat["rapport"] = filestore.audit(database)
|
|
|
|
|
|
|
[ADD] db_restore: check the filestore landed, and open the tool from the menu
Odoo's shutil.move renames when the destination is absent and NESTS when
it exists, so a leftover filestore/<db>/ sends a whole backup into
filestore/<db>/filestore/, where Odoo never looks. That happened once
here and the clone copied it into all seven databases of the chain --
1168 files, 133 MB each, and nothing said a word.
The check runs after a real restore only. A clone copies its source as
it stands, faults included: checking the mirror would say the same thing
twice, and in the wrong place. It warns and names the fix rather than
aborting -- the database is restored and usable, it is the layout that
is wrong.
--- FR ---
Le shutil.move d'Odoo renomme quand la destination est absente et
IMBRIQUE quand elle existe : un filestore/<base>/ resté là envoie toute
une sauvegarde dans filestore/<base>/filestore/, où Odoo ne regarde
jamais. C'est arrivé une fois ici et le clone l'a recopié dans les sept
bases de la chaîne -- 1168 fichiers, 133 Mo chacune, sans un mot.
Le contrôle ne suit qu'une vraie restauration. Un clone recopie sa
source telle quelle, défauts compris : contrôler le miroir dirait deux
fois la même chose, au mauvais endroit. Il avertit et nomme la
correction plutôt que d'interrompre -- la base est restaurée et
utilisable, c'est la disposition qui cloche.
Assisted-by: Claude Opus 5
2026-08-21 22:14:48 -04:00
|
|
|
|
def handler(rank):
|
|
|
|
|
|
if rank == 1:
|
[FIX] filestore: five defects a real repair session brought out
Deduplicating by file was right for COUNTING and wrong for DELETING:
twenty-two rows shared two files, so the purge only ever offered one at
a time and had to be replayed. It now gathers every row whose own field
is dead, and never a live row sharing the same file.
"root" held the root of all filestores instead of this database's
directory, so tidying looked for a nested folder at
<data_dir>/filestore/filestore and answered "nothing to tidy" in front
of 1168 stranded files. It now finds 112 to move up and 1056 duplicates.
limit=0 means "no cap" everywhere else, but the slice [:0] is empty:
"Show every entry" printed "… 3 more" and showed none of them.
The report was captured once, so after a purge it replayed the state
from before -- one then purged rows already gone, believing the work
unfinished. A repair now says whether it did something, and the report
is re-read when it did.
"DELETE 0" was announced as a success. The count now comes from what
PostgreSQL said, and saying nothing is not the same as deleting nothing.
--- FR ---
Dédupliquer par fichier était juste pour COMPTER et faux pour EFFACER :
vingt-deux lignes partageaient deux fichiers, la purge n'en offrait
qu'une à la fois. Elle prend maintenant toutes les lignes dont le champ
est mort, et jamais une ligne vivante partageant le même fichier.
« root » portait la racine de tous les filestores au lieu du dossier de
la base : le rangement cherchait à <data_dir>/filestore/filestore et
répondait « rien à ranger » devant 1168 fichiers échoués. Il en trouve
112 à remonter et 1056 doublons.
limit=0 veut dire « tout » partout ailleurs, mais [:0] est vide : « Tout
afficher » annonçait « … 3 de plus » sans en montrer un seul.
Le rapport n'était lu qu'une fois : après une purge il rejouait l'état
d'avant, et l'on repurgeait des lignes déjà effacées. Une réparation dit
maintenant si elle a fait quelque chose, et le rapport est relu alors.
« DELETE 0 » passait pour un succès. Le compte vient de ce que
PostgreSQL a annoncé, et ne rien dire n'est pas ne rien supprimer.
Assisted-by: Claude Opus 5
2026-08-21 23:20:08 -04:00
|
|
|
|
print("\n".join(filestore.render(etat["rapport"], limit=0)))
|
[ADD] filestore: say if the record still exists, and offer the cleanups
A lost image on a deleted task is not a loss -- nobody will ever look
for it. On a LIVING task it is one, and it is the only one worth
regretting. The report now says which: project.task #15 and
calendar.event #1 both still exist, so those two really are gone.
Three states, not two: "could not check" must not read as "it is gone",
or a real loss gets filed as a false alarm.
Two repairs sit in the follow-up menu. Purging rows whose field no
longer exists deletes by ID, never by a rebuilt domain -- replaying the
reasoning in SQL would open the door to deleting more than was shown.
Tidying the nested filestore moves up what is missing and deletes pure
duplicates, never overwriting a file already in place.
--- FR ---
Une image perdue sur une tâche supprimée n'est pas une perte : personne
ne la cherchera. Sur une tâche VIVANTE, c'en est une, et la seule à
regretter. Le rapport le dit : project.task #15 et calendar.event #1
existent encore, ces deux-là sont bien perdues.
Trois états, pas deux : « pas pu vérifier » ne doit pas se lire « a
disparu », sans quoi une vraie perte passe pour une fausse alerte.
Deux réparations dans le menu de suite. La purge efface par IDENTIFIANT,
jamais par un domaine reconstruit -- rejouer le raisonnement en SQL
ouvrirait la porte à effacer plus que ce qui a été montré. Le rangement
remonte ce qui manque et supprime les doublons purs, sans jamais
écraser un fichier déjà en place.
Assisted-by: Claude Opus 5
2026-08-21 22:39:48 -04:00
|
|
|
|
elif rank == 2:
|
[FIX] filestore: five defects a real repair session brought out
Deduplicating by file was right for COUNTING and wrong for DELETING:
twenty-two rows shared two files, so the purge only ever offered one at
a time and had to be replayed. It now gathers every row whose own field
is dead, and never a live row sharing the same file.
"root" held the root of all filestores instead of this database's
directory, so tidying looked for a nested folder at
<data_dir>/filestore/filestore and answered "nothing to tidy" in front
of 1168 stranded files. It now finds 112 to move up and 1056 duplicates.
limit=0 means "no cap" everywhere else, but the slice [:0] is empty:
"Show every entry" printed "… 3 more" and showed none of them.
The report was captured once, so after a purge it replayed the state
from before -- one then purged rows already gone, believing the work
unfinished. A repair now says whether it did something, and the report
is re-read when it did.
"DELETE 0" was announced as a success. The count now comes from what
PostgreSQL said, and saying nothing is not the same as deleting nothing.
--- FR ---
Dédupliquer par fichier était juste pour COMPTER et faux pour EFFACER :
vingt-deux lignes partageaient deux fichiers, la purge n'en offrait
qu'une à la fois. Elle prend maintenant toutes les lignes dont le champ
est mort, et jamais une ligne vivante partageant le même fichier.
« root » portait la racine de tous les filestores au lieu du dossier de
la base : le rangement cherchait à <data_dir>/filestore/filestore et
répondait « rien à ranger » devant 1168 fichiers échoués. Il en trouve
112 à remonter et 1056 doublons.
limit=0 veut dire « tout » partout ailleurs, mais [:0] est vide : « Tout
afficher » annonçait « … 3 de plus » sans en montrer un seul.
Le rapport n'était lu qu'une fois : après une purge il rejouait l'état
d'avant, et l'on repurgeait des lignes déjà effacées. Une réparation dit
maintenant si elle a fait quelque chose, et le rapport est relu alors.
« DELETE 0 » passait pour un succès. Le compte vient de ce que
PostgreSQL a annoncé, et ne rien dire n'est pas ne rien supprimer.
Assisted-by: Claude Opus 5
2026-08-21 23:20:08 -04:00
|
|
|
|
if self._filestore_purge_dead(database, etat["rapport"]):
|
|
|
|
|
|
relire()
|
[ADD] filestore: say if the record still exists, and offer the cleanups
A lost image on a deleted task is not a loss -- nobody will ever look
for it. On a LIVING task it is one, and it is the only one worth
regretting. The report now says which: project.task #15 and
calendar.event #1 both still exist, so those two really are gone.
Three states, not two: "could not check" must not read as "it is gone",
or a real loss gets filed as a false alarm.
Two repairs sit in the follow-up menu. Purging rows whose field no
longer exists deletes by ID, never by a rebuilt domain -- replaying the
reasoning in SQL would open the door to deleting more than was shown.
Tidying the nested filestore moves up what is missing and deletes pure
duplicates, never overwriting a file already in place.
--- FR ---
Une image perdue sur une tâche supprimée n'est pas une perte : personne
ne la cherchera. Sur une tâche VIVANTE, c'en est une, et la seule à
regretter. Le rapport le dit : project.task #15 et calendar.event #1
existent encore, ces deux-là sont bien perdues.
Trois états, pas deux : « pas pu vérifier » ne doit pas se lire « a
disparu », sans quoi une vraie perte passe pour une fausse alerte.
Deux réparations dans le menu de suite. La purge efface par IDENTIFIANT,
jamais par un domaine reconstruit -- rejouer le raisonnement en SQL
ouvrirait la porte à effacer plus que ce qui a été montré. Le rangement
remonte ce qui manque et supprime les doublons purs, sans jamais
écraser un fichier déjà en place.
Assisted-by: Claude Opus 5
2026-08-21 22:39:48 -04:00
|
|
|
|
elif rank == 3:
|
[FIX] filestore: five defects a real repair session brought out
Deduplicating by file was right for COUNTING and wrong for DELETING:
twenty-two rows shared two files, so the purge only ever offered one at
a time and had to be replayed. It now gathers every row whose own field
is dead, and never a live row sharing the same file.
"root" held the root of all filestores instead of this database's
directory, so tidying looked for a nested folder at
<data_dir>/filestore/filestore and answered "nothing to tidy" in front
of 1168 stranded files. It now finds 112 to move up and 1056 duplicates.
limit=0 means "no cap" everywhere else, but the slice [:0] is empty:
"Show every entry" printed "… 3 more" and showed none of them.
The report was captured once, so after a purge it replayed the state
from before -- one then purged rows already gone, believing the work
unfinished. A repair now says whether it did something, and the report
is re-read when it did.
"DELETE 0" was announced as a success. The count now comes from what
PostgreSQL said, and saying nothing is not the same as deleting nothing.
--- FR ---
Dédupliquer par fichier était juste pour COMPTER et faux pour EFFACER :
vingt-deux lignes partageaient deux fichiers, la purge n'en offrait
qu'une à la fois. Elle prend maintenant toutes les lignes dont le champ
est mort, et jamais une ligne vivante partageant le même fichier.
« root » portait la racine de tous les filestores au lieu du dossier de
la base : le rangement cherchait à <data_dir>/filestore/filestore et
répondait « rien à ranger » devant 1168 fichiers échoués. Il en trouve
112 à remonter et 1056 doublons.
limit=0 veut dire « tout » partout ailleurs, mais [:0] est vide : « Tout
afficher » annonçait « … 3 de plus » sans en montrer un seul.
Le rapport n'était lu qu'une fois : après une purge il rejouait l'état
d'avant, et l'on repurgeait des lignes déjà effacées. Une réparation dit
maintenant si elle a fait quelque chose, et le rapport est relu alors.
« DELETE 0 » passait pour un succès. Le compte vient de ce que
PostgreSQL a annoncé, et ne rien dire n'est pas ne rien supprimer.
Assisted-by: Claude Opus 5
2026-08-21 23:20:08 -04:00
|
|
|
|
if self._filestore_tidy_nested(etat["rapport"]):
|
|
|
|
|
|
relire()
|
[ADD] db_restore: check the filestore landed, and open the tool from the menu
Odoo's shutil.move renames when the destination is absent and NESTS when
it exists, so a leftover filestore/<db>/ sends a whole backup into
filestore/<db>/filestore/, where Odoo never looks. That happened once
here and the clone copied it into all seven databases of the chain --
1168 files, 133 MB each, and nothing said a word.
The check runs after a real restore only. A clone copies its source as
it stands, faults included: checking the mirror would say the same thing
twice, and in the wrong place. It warns and names the fix rather than
aborting -- the database is restored and usable, it is the layout that
is wrong.
--- FR ---
Le shutil.move d'Odoo renomme quand la destination est absente et
IMBRIQUE quand elle existe : un filestore/<base>/ resté là envoie toute
une sauvegarde dans filestore/<base>/filestore/, où Odoo ne regarde
jamais. C'est arrivé une fois ici et le clone l'a recopié dans les sept
bases de la chaîne -- 1168 fichiers, 133 Mo chacune, sans un mot.
Le contrôle ne suit qu'une vraie restauration. Un clone recopie sa
source telle quelle, défauts compris : contrôler le miroir dirait deux
fois la même chose, au mauvais endroit. Il avertit et nomme la
correction plutôt que d'interrompre -- la base est restaurée et
utilisable, c'est la disposition qui cloche.
Assisted-by: Claude Opus 5
2026-08-21 22:14:48 -04:00
|
|
|
|
else:
|
|
|
|
|
|
self._analyse_export_json(
|
[FIX] filestore: five defects a real repair session brought out
Deduplicating by file was right for COUNTING and wrong for DELETING:
twenty-two rows shared two files, so the purge only ever offered one at
a time and had to be replayed. It now gathers every row whose own field
is dead, and never a live row sharing the same file.
"root" held the root of all filestores instead of this database's
directory, so tidying looked for a nested folder at
<data_dir>/filestore/filestore and answered "nothing to tidy" in front
of 1168 stranded files. It now finds 112 to move up and 1056 duplicates.
limit=0 means "no cap" everywhere else, but the slice [:0] is empty:
"Show every entry" printed "… 3 more" and showed none of them.
The report was captured once, so after a purge it replayed the state
from before -- one then purged rows already gone, believing the work
unfinished. A repair now says whether it did something, and the report
is re-read when it did.
"DELETE 0" was announced as a success. The count now comes from what
PostgreSQL said, and saying nothing is not the same as deleting nothing.
--- FR ---
Dédupliquer par fichier était juste pour COMPTER et faux pour EFFACER :
vingt-deux lignes partageaient deux fichiers, la purge n'en offrait
qu'une à la fois. Elle prend maintenant toutes les lignes dont le champ
est mort, et jamais une ligne vivante partageant le même fichier.
« root » portait la racine de tous les filestores au lieu du dossier de
la base : le rangement cherchait à <data_dir>/filestore/filestore et
répondait « rien à ranger » devant 1168 fichiers échoués. Il en trouve
112 à remonter et 1056 doublons.
limit=0 veut dire « tout » partout ailleurs, mais [:0] est vide : « Tout
afficher » annonçait « … 3 de plus » sans en montrer un seul.
Le rapport n'était lu qu'une fois : après une purge il rejouait l'état
d'avant, et l'on repurgeait des lignes déjà effacées. Une réparation dit
maintenant si elle a fait quelque chose, et le rapport est relu alors.
« DELETE 0 » passait pour un succès. Le compte vient de ce que
PostgreSQL a annoncé, et ne rien dire n'est pas ne rien supprimer.
Assisted-by: Claude Opus 5
2026-08-21 23:20:08 -04:00
|
|
|
|
etat["rapport"], os.path.basename(database), "filestore"
|
[ADD] db_restore: check the filestore landed, and open the tool from the menu
Odoo's shutil.move renames when the destination is absent and NESTS when
it exists, so a leftover filestore/<db>/ sends a whole backup into
filestore/<db>/filestore/, where Odoo never looks. That happened once
here and the clone copied it into all seven databases of the chain --
1168 files, 133 MB each, and nothing said a word.
The check runs after a real restore only. A clone copies its source as
it stands, faults included: checking the mirror would say the same thing
twice, and in the wrong place. It warns and names the fix rather than
aborting -- the database is restored and usable, it is the layout that
is wrong.
--- FR ---
Le shutil.move d'Odoo renomme quand la destination est absente et
IMBRIQUE quand elle existe : un filestore/<base>/ resté là envoie toute
une sauvegarde dans filestore/<base>/filestore/, où Odoo ne regarde
jamais. C'est arrivé une fois ici et le clone l'a recopié dans les sept
bases de la chaîne -- 1168 fichiers, 133 Mo chacune, sans un mot.
Le contrôle ne suit qu'une vraie restauration. Un clone recopie sa
source telle quelle, défauts compris : contrôler le miroir dirait deux
fois la même chose, au mauvais endroit. Il avertit et nomme la
correction plutôt que d'interrompre -- la base est restaurée et
utilisable, c'est la disposition qui cloche.
Assisted-by: Claude Opus 5
2026-08-21 22:14:48 -04:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
self._analyse_follow_up(
|
|
|
|
|
|
[
|
|
|
|
|
|
{"prompt_description": t("Show every entry")},
|
[ADD] filestore: say if the record still exists, and offer the cleanups
A lost image on a deleted task is not a loss -- nobody will ever look
for it. On a LIVING task it is one, and it is the only one worth
regretting. The report now says which: project.task #15 and
calendar.event #1 both still exist, so those two really are gone.
Three states, not two: "could not check" must not read as "it is gone",
or a real loss gets filed as a false alarm.
Two repairs sit in the follow-up menu. Purging rows whose field no
longer exists deletes by ID, never by a rebuilt domain -- replaying the
reasoning in SQL would open the door to deleting more than was shown.
Tidying the nested filestore moves up what is missing and deletes pure
duplicates, never overwriting a file already in place.
--- FR ---
Une image perdue sur une tâche supprimée n'est pas une perte : personne
ne la cherchera. Sur une tâche VIVANTE, c'en est une, et la seule à
regretter. Le rapport le dit : project.task #15 et calendar.event #1
existent encore, ces deux-là sont bien perdues.
Trois états, pas deux : « pas pu vérifier » ne doit pas se lire « a
disparu », sans quoi une vraie perte passe pour une fausse alerte.
Deux réparations dans le menu de suite. La purge efface par IDENTIFIANT,
jamais par un domaine reconstruit -- rejouer le raisonnement en SQL
ouvrirait la porte à effacer plus que ce qui a été montré. Le rangement
remonte ce qui manque et supprime les doublons purs, sans jamais
écraser un fichier déjà en place.
Assisted-by: Claude Opus 5
2026-08-21 22:39:48 -04:00
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
|
"🧹 Purge attachments whose field no longer exists"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
|
"🧹 Tidy the nested filestore Odoo never reads"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
[ADD] db_restore: check the filestore landed, and open the tool from the menu
Odoo's shutil.move renames when the destination is absent and NESTS when
it exists, so a leftover filestore/<db>/ sends a whole backup into
filestore/<db>/filestore/, where Odoo never looks. That happened once
here and the clone copied it into all seven databases of the chain --
1168 files, 133 MB each, and nothing said a word.
The check runs after a real restore only. A clone copies its source as
it stands, faults included: checking the mirror would say the same thing
twice, and in the wrong place. It warns and names the fix rather than
aborting -- the database is restored and usable, it is the layout that
is wrong.
--- FR ---
Le shutil.move d'Odoo renomme quand la destination est absente et
IMBRIQUE quand elle existe : un filestore/<base>/ resté là envoie toute
une sauvegarde dans filestore/<base>/filestore/, où Odoo ne regarde
jamais. C'est arrivé une fois ici et le clone l'a recopié dans les sept
bases de la chaîne -- 1168 fichiers, 133 Mo chacune, sans un mot.
Le contrôle ne suit qu'une vraie restauration. Un clone recopie sa
source telle quelle, défauts compris : contrôler le miroir dirait deux
fois la même chose, au mauvais endroit. Il avertit et nomme la
correction plutôt que d'interrompre -- la base est restaurée et
utilisable, c'est la disposition qui cloche.
Assisted-by: Claude Opus 5
2026-08-21 22:14:48 -04:00
|
|
|
|
{"prompt_description": t("Export as JSON")},
|
|
|
|
|
|
],
|
|
|
|
|
|
handler,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
[ADD] filestore: say if the record still exists, and offer the cleanups
A lost image on a deleted task is not a loss -- nobody will ever look
for it. On a LIVING task it is one, and it is the only one worth
regretting. The report now says which: project.task #15 and
calendar.event #1 both still exist, so those two really are gone.
Three states, not two: "could not check" must not read as "it is gone",
or a real loss gets filed as a false alarm.
Two repairs sit in the follow-up menu. Purging rows whose field no
longer exists deletes by ID, never by a rebuilt domain -- replaying the
reasoning in SQL would open the door to deleting more than was shown.
Tidying the nested filestore moves up what is missing and deletes pure
duplicates, never overwriting a file already in place.
--- FR ---
Une image perdue sur une tâche supprimée n'est pas une perte : personne
ne la cherchera. Sur une tâche VIVANTE, c'en est une, et la seule à
regretter. Le rapport le dit : project.task #15 et calendar.event #1
existent encore, ces deux-là sont bien perdues.
Trois états, pas deux : « pas pu vérifier » ne doit pas se lire « a
disparu », sans quoi une vraie perte passe pour une fausse alerte.
Deux réparations dans le menu de suite. La purge efface par IDENTIFIANT,
jamais par un domaine reconstruit -- rejouer le raisonnement en SQL
ouvrirait la porte à effacer plus que ce qui a été montré. Le rangement
remonte ce qui manque et supprime les doublons purs, sans jamais
écraser un fichier déjà en place.
Assisted-by: Claude Opus 5
2026-08-21 22:39:48 -04:00
|
|
|
|
def _filestore_purge_dead(self, database, rapport):
|
|
|
|
|
|
"""Effacer les pièces jointes dont le champ a disparu.
|
|
|
|
|
|
|
|
|
|
|
|
La seule ÉCRITURE en base de tout le menu Analyse. Elle porte sur
|
|
|
|
|
|
des lignes que plus rien ne lit — `res.country.image` est devenu
|
|
|
|
|
|
`image_url`, calculé, en 13 — mais elle reste une suppression :
|
|
|
|
|
|
question explicite, défaut à « non », et le compte est relu avant
|
|
|
|
|
|
de partir.
|
|
|
|
|
|
"""
|
|
|
|
|
|
from script.analyse import check_filestore as filestore
|
|
|
|
|
|
from script.todo import auto_ask
|
|
|
|
|
|
|
|
|
|
|
|
lignes = rapport["groups"]["dead_field"]
|
|
|
|
|
|
sql = filestore.purge_dead_sql(rapport)
|
|
|
|
|
|
if not sql:
|
|
|
|
|
|
print(f"ℹ️ {t('Nothing to purge.')}")
|
[FIX] filestore: five defects a real repair session brought out
Deduplicating by file was right for COUNTING and wrong for DELETING:
twenty-two rows shared two files, so the purge only ever offered one at
a time and had to be replayed. It now gathers every row whose own field
is dead, and never a live row sharing the same file.
"root" held the root of all filestores instead of this database's
directory, so tidying looked for a nested folder at
<data_dir>/filestore/filestore and answered "nothing to tidy" in front
of 1168 stranded files. It now finds 112 to move up and 1056 duplicates.
limit=0 means "no cap" everywhere else, but the slice [:0] is empty:
"Show every entry" printed "… 3 more" and showed none of them.
The report was captured once, so after a purge it replayed the state
from before -- one then purged rows already gone, believing the work
unfinished. A repair now says whether it did something, and the report
is re-read when it did.
"DELETE 0" was announced as a success. The count now comes from what
PostgreSQL said, and saying nothing is not the same as deleting nothing.
--- FR ---
Dédupliquer par fichier était juste pour COMPTER et faux pour EFFACER :
vingt-deux lignes partageaient deux fichiers, la purge n'en offrait
qu'une à la fois. Elle prend maintenant toutes les lignes dont le champ
est mort, et jamais une ligne vivante partageant le même fichier.
« root » portait la racine de tous les filestores au lieu du dossier de
la base : le rangement cherchait à <data_dir>/filestore/filestore et
répondait « rien à ranger » devant 1168 fichiers échoués. Il en trouve
112 à remonter et 1056 doublons.
limit=0 veut dire « tout » partout ailleurs, mais [:0] est vide : « Tout
afficher » annonçait « … 3 de plus » sans en montrer un seul.
Le rapport n'était lu qu'une fois : après une purge il rejouait l'état
d'avant, et l'on repurgeait des lignes déjà effacées. Une réparation dit
maintenant si elle a fait quelque chose, et le rapport est relu alors.
« DELETE 0 » passait pour un succès. Le compte vient de ce que
PostgreSQL a annoncé, et ne rien dire n'est pas ne rien supprimer.
Assisted-by: Claude Opus 5
2026-08-21 23:20:08 -04:00
|
|
|
|
return False
|
[ADD] filestore: say if the record still exists, and offer the cleanups
A lost image on a deleted task is not a loss -- nobody will ever look
for it. On a LIVING task it is one, and it is the only one worth
regretting. The report now says which: project.task #15 and
calendar.event #1 both still exist, so those two really are gone.
Three states, not two: "could not check" must not read as "it is gone",
or a real loss gets filed as a false alarm.
Two repairs sit in the follow-up menu. Purging rows whose field no
longer exists deletes by ID, never by a rebuilt domain -- replaying the
reasoning in SQL would open the door to deleting more than was shown.
Tidying the nested filestore moves up what is missing and deletes pure
duplicates, never overwriting a file already in place.
--- FR ---
Une image perdue sur une tâche supprimée n'est pas une perte : personne
ne la cherchera. Sur une tâche VIVANTE, c'en est une, et la seule à
regretter. Le rapport le dit : project.task #15 et calendar.event #1
existent encore, ces deux-là sont bien perdues.
Trois états, pas deux : « pas pu vérifier » ne doit pas se lire « a
disparu », sans quoi une vraie perte passe pour une fausse alerte.
Deux réparations dans le menu de suite. La purge efface par IDENTIFIANT,
jamais par un domaine reconstruit -- rejouer le raisonnement en SQL
ouvrirait la porte à effacer plus que ce qui a été montré. Le rangement
remonte ce qui manque et supprime les doublons purs, sans jamais
écraser un fichier déjà en place.
Assisted-by: Claude Opus 5
2026-08-21 22:39:48 -04:00
|
|
|
|
print()
|
|
|
|
|
|
for texte in filestore.summarise(lignes):
|
|
|
|
|
|
print(f" {texte}")
|
|
|
|
|
|
question = (
|
|
|
|
|
|
f"💬 {t('Delete these')} {len(lignes)}"
|
|
|
|
|
|
f" {t('attachment row(s) for good?')} (y/N) : "
|
|
|
|
|
|
)
|
|
|
|
|
|
if auto_ask.ask(question, default="n").strip().lower() not in (
|
|
|
|
|
|
"y",
|
|
|
|
|
|
"yes",
|
|
|
|
|
|
"o",
|
|
|
|
|
|
):
|
|
|
|
|
|
print(f"ℹ️ {t('Nothing was deleted.')}")
|
[FIX] filestore: five defects a real repair session brought out
Deduplicating by file was right for COUNTING and wrong for DELETING:
twenty-two rows shared two files, so the purge only ever offered one at
a time and had to be replayed. It now gathers every row whose own field
is dead, and never a live row sharing the same file.
"root" held the root of all filestores instead of this database's
directory, so tidying looked for a nested folder at
<data_dir>/filestore/filestore and answered "nothing to tidy" in front
of 1168 stranded files. It now finds 112 to move up and 1056 duplicates.
limit=0 means "no cap" everywhere else, but the slice [:0] is empty:
"Show every entry" printed "… 3 more" and showed none of them.
The report was captured once, so after a purge it replayed the state
from before -- one then purged rows already gone, believing the work
unfinished. A repair now says whether it did something, and the report
is re-read when it did.
"DELETE 0" was announced as a success. The count now comes from what
PostgreSQL said, and saying nothing is not the same as deleting nothing.
--- FR ---
Dédupliquer par fichier était juste pour COMPTER et faux pour EFFACER :
vingt-deux lignes partageaient deux fichiers, la purge n'en offrait
qu'une à la fois. Elle prend maintenant toutes les lignes dont le champ
est mort, et jamais une ligne vivante partageant le même fichier.
« root » portait la racine de tous les filestores au lieu du dossier de
la base : le rangement cherchait à <data_dir>/filestore/filestore et
répondait « rien à ranger » devant 1168 fichiers échoués. Il en trouve
112 à remonter et 1056 doublons.
limit=0 veut dire « tout » partout ailleurs, mais [:0] est vide : « Tout
afficher » annonçait « … 3 de plus » sans en montrer un seul.
Le rapport n'était lu qu'une fois : après une purge il rejouait l'état
d'avant, et l'on repurgeait des lignes déjà effacées. Une réparation dit
maintenant si elle a fait quelque chose, et le rapport est relu alors.
« DELETE 0 » passait pour un succès. Le compte vient de ce que
PostgreSQL a annoncé, et ne rien dire n'est pas ne rien supprimer.
Assisted-by: Claude Opus 5
2026-08-21 23:20:08 -04:00
|
|
|
|
return False
|
|
|
|
|
|
status, sortie = self.execute.exec_command_live(
|
[ADD] filestore: say if the record still exists, and offer the cleanups
A lost image on a deleted task is not a loss -- nobody will ever look
for it. On a LIVING task it is one, and it is the only one worth
regretting. The report now says which: project.task #15 and
calendar.event #1 both still exist, so those two really are gone.
Three states, not two: "could not check" must not read as "it is gone",
or a real loss gets filed as a false alarm.
Two repairs sit in the follow-up menu. Purging rows whose field no
longer exists deletes by ID, never by a rebuilt domain -- replaying the
reasoning in SQL would open the door to deleting more than was shown.
Tidying the nested filestore moves up what is missing and deletes pure
duplicates, never overwriting a file already in place.
--- FR ---
Une image perdue sur une tâche supprimée n'est pas une perte : personne
ne la cherchera. Sur une tâche VIVANTE, c'en est une, et la seule à
regretter. Le rapport le dit : project.task #15 et calendar.event #1
existent encore, ces deux-là sont bien perdues.
Trois états, pas deux : « pas pu vérifier » ne doit pas se lire « a
disparu », sans quoi une vraie perte passe pour une fausse alerte.
Deux réparations dans le menu de suite. La purge efface par IDENTIFIANT,
jamais par un domaine reconstruit -- rejouer le raisonnement en SQL
ouvrirait la porte à effacer plus que ce qui a été montré. Le rangement
remonte ce qui manque et supprime les doublons purs, sans jamais
écraser un fichier déjà en place.
Assisted-by: Claude Opus 5
2026-08-21 22:39:48 -04:00
|
|
|
|
f'psql -d {database} -c "{sql}"',
|
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
single_source_erplibre=True,
|
[FIX] filestore: five defects a real repair session brought out
Deduplicating by file was right for COUNTING and wrong for DELETING:
twenty-two rows shared two files, so the purge only ever offered one at
a time and had to be replayed. It now gathers every row whose own field
is dead, and never a live row sharing the same file.
"root" held the root of all filestores instead of this database's
directory, so tidying looked for a nested folder at
<data_dir>/filestore/filestore and answered "nothing to tidy" in front
of 1168 stranded files. It now finds 112 to move up and 1056 duplicates.
limit=0 means "no cap" everywhere else, but the slice [:0] is empty:
"Show every entry" printed "… 3 more" and showed none of them.
The report was captured once, so after a purge it replayed the state
from before -- one then purged rows already gone, believing the work
unfinished. A repair now says whether it did something, and the report
is re-read when it did.
"DELETE 0" was announced as a success. The count now comes from what
PostgreSQL said, and saying nothing is not the same as deleting nothing.
--- FR ---
Dédupliquer par fichier était juste pour COMPTER et faux pour EFFACER :
vingt-deux lignes partageaient deux fichiers, la purge n'en offrait
qu'une à la fois. Elle prend maintenant toutes les lignes dont le champ
est mort, et jamais une ligne vivante partageant le même fichier.
« root » portait la racine de tous les filestores au lieu du dossier de
la base : le rangement cherchait à <data_dir>/filestore/filestore et
répondait « rien à ranger » devant 1168 fichiers échoués. Il en trouve
112 à remonter et 1056 doublons.
limit=0 veut dire « tout » partout ailleurs, mais [:0] est vide : « Tout
afficher » annonçait « … 3 de plus » sans en montrer un seul.
Le rapport n'était lu qu'une fois : après une purge il rejouait l'état
d'avant, et l'on repurgeait des lignes déjà effacées. Une réparation dit
maintenant si elle a fait quelque chose, et le rapport est relu alors.
« DELETE 0 » passait pour un succès. Le compte vient de ce que
PostgreSQL a annoncé, et ne rien dire n'est pas ne rien supprimer.
Assisted-by: Claude Opus 5
2026-08-21 23:20:08 -04:00
|
|
|
|
return_status_and_output=True,
|
[ADD] filestore: say if the record still exists, and offer the cleanups
A lost image on a deleted task is not a loss -- nobody will ever look
for it. On a LIVING task it is one, and it is the only one worth
regretting. The report now says which: project.task #15 and
calendar.event #1 both still exist, so those two really are gone.
Three states, not two: "could not check" must not read as "it is gone",
or a real loss gets filed as a false alarm.
Two repairs sit in the follow-up menu. Purging rows whose field no
longer exists deletes by ID, never by a rebuilt domain -- replaying the
reasoning in SQL would open the door to deleting more than was shown.
Tidying the nested filestore moves up what is missing and deletes pure
duplicates, never overwriting a file already in place.
--- FR ---
Une image perdue sur une tâche supprimée n'est pas une perte : personne
ne la cherchera. Sur une tâche VIVANTE, c'en est une, et la seule à
regretter. Le rapport le dit : project.task #15 et calendar.event #1
existent encore, ces deux-là sont bien perdues.
Trois états, pas deux : « pas pu vérifier » ne doit pas se lire « a
disparu », sans quoi une vraie perte passe pour une fausse alerte.
Deux réparations dans le menu de suite. La purge efface par IDENTIFIANT,
jamais par un domaine reconstruit -- rejouer le raisonnement en SQL
ouvrirait la porte à effacer plus que ce qui a été montré. Le rangement
remonte ce qui manque et supprime les doublons purs, sans jamais
écraser un fichier déjà en place.
Assisted-by: Claude Opus 5
2026-08-21 22:39:48 -04:00
|
|
|
|
)
|
|
|
|
|
|
if status:
|
|
|
|
|
|
print(f"❌ {t('The purge failed.')}")
|
[FIX] filestore: five defects a real repair session brought out
Deduplicating by file was right for COUNTING and wrong for DELETING:
twenty-two rows shared two files, so the purge only ever offered one at
a time and had to be replayed. It now gathers every row whose own field
is dead, and never a live row sharing the same file.
"root" held the root of all filestores instead of this database's
directory, so tidying looked for a nested folder at
<data_dir>/filestore/filestore and answered "nothing to tidy" in front
of 1168 stranded files. It now finds 112 to move up and 1056 duplicates.
limit=0 means "no cap" everywhere else, but the slice [:0] is empty:
"Show every entry" printed "… 3 more" and showed none of them.
The report was captured once, so after a purge it replayed the state
from before -- one then purged rows already gone, believing the work
unfinished. A repair now says whether it did something, and the report
is re-read when it did.
"DELETE 0" was announced as a success. The count now comes from what
PostgreSQL said, and saying nothing is not the same as deleting nothing.
--- FR ---
Dédupliquer par fichier était juste pour COMPTER et faux pour EFFACER :
vingt-deux lignes partageaient deux fichiers, la purge n'en offrait
qu'une à la fois. Elle prend maintenant toutes les lignes dont le champ
est mort, et jamais une ligne vivante partageant le même fichier.
« root » portait la racine de tous les filestores au lieu du dossier de
la base : le rangement cherchait à <data_dir>/filestore/filestore et
répondait « rien à ranger » devant 1168 fichiers échoués. Il en trouve
112 à remonter et 1056 doublons.
limit=0 veut dire « tout » partout ailleurs, mais [:0] est vide : « Tout
afficher » annonçait « … 3 de plus » sans en montrer un seul.
Le rapport n'était lu qu'une fois : après une purge il rejouait l'état
d'avant, et l'on repurgeait des lignes déjà effacées. Une réparation dit
maintenant si elle a fait quelque chose, et le rapport est relu alors.
« DELETE 0 » passait pour un succès. Le compte vient de ce que
PostgreSQL a annoncé, et ne rien dire n'est pas ne rien supprimer.
Assisted-by: Claude Opus 5
2026-08-21 23:20:08 -04:00
|
|
|
|
return False
|
|
|
|
|
|
# Le nombre ANNONCÉ par PostgreSQL, pas celui qu'on espérait :
|
|
|
|
|
|
# rejouer une purge déjà faite rendait « DELETE 0 » et l'outil
|
|
|
|
|
|
# se félicitait quand même d'avoir supprimé.
|
|
|
|
|
|
efface = filestore.rows_deleted(sortie)
|
|
|
|
|
|
if efface is None:
|
|
|
|
|
|
print(f"⚠ {t('The purge ran but said nothing.')}")
|
|
|
|
|
|
return True
|
|
|
|
|
|
print(f"✅ {efface} {t('attachment row(s) deleted.')}")
|
|
|
|
|
|
return True
|
[ADD] filestore: say if the record still exists, and offer the cleanups
A lost image on a deleted task is not a loss -- nobody will ever look
for it. On a LIVING task it is one, and it is the only one worth
regretting. The report now says which: project.task #15 and
calendar.event #1 both still exist, so those two really are gone.
Three states, not two: "could not check" must not read as "it is gone",
or a real loss gets filed as a false alarm.
Two repairs sit in the follow-up menu. Purging rows whose field no
longer exists deletes by ID, never by a rebuilt domain -- replaying the
reasoning in SQL would open the door to deleting more than was shown.
Tidying the nested filestore moves up what is missing and deletes pure
duplicates, never overwriting a file already in place.
--- FR ---
Une image perdue sur une tâche supprimée n'est pas une perte : personne
ne la cherchera. Sur une tâche VIVANTE, c'en est une, et la seule à
regretter. Le rapport le dit : project.task #15 et calendar.event #1
existent encore, ces deux-là sont bien perdues.
Trois états, pas deux : « pas pu vérifier » ne doit pas se lire « a
disparu », sans quoi une vraie perte passe pour une fausse alerte.
Deux réparations dans le menu de suite. La purge efface par IDENTIFIANT,
jamais par un domaine reconstruit -- rejouer le raisonnement en SQL
ouvrirait la porte à effacer plus que ce qui a été montré. Le rangement
remonte ce qui manque et supprime les doublons purs, sans jamais
écraser un fichier déjà en place.
Assisted-by: Claude Opus 5
2026-08-21 22:39:48 -04:00
|
|
|
|
|
|
|
|
|
|
def _filestore_tidy_nested(self, rapport):
|
|
|
|
|
|
"""Remonter ce qui manque, effacer les doublons purs.
|
|
|
|
|
|
|
|
|
|
|
|
Deux tas, deux gestes. Écraser un fichier présent par une copie
|
|
|
|
|
|
identique ne gagnerait rien et brouillerait la trace ; c'est
|
|
|
|
|
|
pourquoi les doublons sont comptés à part et jamais déplacés.
|
|
|
|
|
|
"""
|
|
|
|
|
|
from script.analyse import check_filestore as filestore
|
|
|
|
|
|
from script.todo import auto_ask
|
|
|
|
|
|
|
|
|
|
|
|
remonter, doublons = filestore.tidy_nested_plan(rapport)
|
|
|
|
|
|
if not remonter and not doublons:
|
|
|
|
|
|
print(f"ℹ️ {t('No nested filestore to tidy.')}")
|
[FIX] filestore: five defects a real repair session brought out
Deduplicating by file was right for COUNTING and wrong for DELETING:
twenty-two rows shared two files, so the purge only ever offered one at
a time and had to be replayed. It now gathers every row whose own field
is dead, and never a live row sharing the same file.
"root" held the root of all filestores instead of this database's
directory, so tidying looked for a nested folder at
<data_dir>/filestore/filestore and answered "nothing to tidy" in front
of 1168 stranded files. It now finds 112 to move up and 1056 duplicates.
limit=0 means "no cap" everywhere else, but the slice [:0] is empty:
"Show every entry" printed "… 3 more" and showed none of them.
The report was captured once, so after a purge it replayed the state
from before -- one then purged rows already gone, believing the work
unfinished. A repair now says whether it did something, and the report
is re-read when it did.
"DELETE 0" was announced as a success. The count now comes from what
PostgreSQL said, and saying nothing is not the same as deleting nothing.
--- FR ---
Dédupliquer par fichier était juste pour COMPTER et faux pour EFFACER :
vingt-deux lignes partageaient deux fichiers, la purge n'en offrait
qu'une à la fois. Elle prend maintenant toutes les lignes dont le champ
est mort, et jamais une ligne vivante partageant le même fichier.
« root » portait la racine de tous les filestores au lieu du dossier de
la base : le rangement cherchait à <data_dir>/filestore/filestore et
répondait « rien à ranger » devant 1168 fichiers échoués. Il en trouve
112 à remonter et 1056 doublons.
limit=0 veut dire « tout » partout ailleurs, mais [:0] est vide : « Tout
afficher » annonçait « … 3 de plus » sans en montrer un seul.
Le rapport n'était lu qu'une fois : après une purge il rejouait l'état
d'avant, et l'on repurgeait des lignes déjà effacées. Une réparation dit
maintenant si elle a fait quelque chose, et le rapport est relu alors.
« DELETE 0 » passait pour un succès. Le compte vient de ce que
PostgreSQL a annoncé, et ne rien dire n'est pas ne rien supprimer.
Assisted-by: Claude Opus 5
2026-08-21 23:20:08 -04:00
|
|
|
|
return False
|
[ADD] filestore: say if the record still exists, and offer the cleanups
A lost image on a deleted task is not a loss -- nobody will ever look
for it. On a LIVING task it is one, and it is the only one worth
regretting. The report now says which: project.task #15 and
calendar.event #1 both still exist, so those two really are gone.
Three states, not two: "could not check" must not read as "it is gone",
or a real loss gets filed as a false alarm.
Two repairs sit in the follow-up menu. Purging rows whose field no
longer exists deletes by ID, never by a rebuilt domain -- replaying the
reasoning in SQL would open the door to deleting more than was shown.
Tidying the nested filestore moves up what is missing and deletes pure
duplicates, never overwriting a file already in place.
--- FR ---
Une image perdue sur une tâche supprimée n'est pas une perte : personne
ne la cherchera. Sur une tâche VIVANTE, c'en est une, et la seule à
regretter. Le rapport le dit : project.task #15 et calendar.event #1
existent encore, ces deux-là sont bien perdues.
Trois états, pas deux : « pas pu vérifier » ne doit pas se lire « a
disparu », sans quoi une vraie perte passe pour une fausse alerte.
Deux réparations dans le menu de suite. La purge efface par IDENTIFIANT,
jamais par un domaine reconstruit -- rejouer le raisonnement en SQL
ouvrirait la porte à effacer plus que ce qui a été montré. Le rangement
remonte ce qui manque et supprime les doublons purs, sans jamais
écraser un fichier déjà en place.
Assisted-by: Claude Opus 5
2026-08-21 22:39:48 -04:00
|
|
|
|
print()
|
|
|
|
|
|
print(f" {len(remonter)} {t('file(s) to move up')}")
|
|
|
|
|
|
print(f" {len(doublons)} {t('pure duplicate(s) to delete')}")
|
|
|
|
|
|
print(f" {t('Directory:')} {filestore.nested_dir(rapport)}")
|
|
|
|
|
|
if auto_ask.ask(
|
|
|
|
|
|
f"💬 {t('Go ahead?')} (y/N) : ", default="n"
|
|
|
|
|
|
).strip().lower() not in ("y", "yes", "o"):
|
|
|
|
|
|
print(f"ℹ️ {t('Nothing was moved.')}")
|
[FIX] filestore: five defects a real repair session brought out
Deduplicating by file was right for COUNTING and wrong for DELETING:
twenty-two rows shared two files, so the purge only ever offered one at
a time and had to be replayed. It now gathers every row whose own field
is dead, and never a live row sharing the same file.
"root" held the root of all filestores instead of this database's
directory, so tidying looked for a nested folder at
<data_dir>/filestore/filestore and answered "nothing to tidy" in front
of 1168 stranded files. It now finds 112 to move up and 1056 duplicates.
limit=0 means "no cap" everywhere else, but the slice [:0] is empty:
"Show every entry" printed "… 3 more" and showed none of them.
The report was captured once, so after a purge it replayed the state
from before -- one then purged rows already gone, believing the work
unfinished. A repair now says whether it did something, and the report
is re-read when it did.
"DELETE 0" was announced as a success. The count now comes from what
PostgreSQL said, and saying nothing is not the same as deleting nothing.
--- FR ---
Dédupliquer par fichier était juste pour COMPTER et faux pour EFFACER :
vingt-deux lignes partageaient deux fichiers, la purge n'en offrait
qu'une à la fois. Elle prend maintenant toutes les lignes dont le champ
est mort, et jamais une ligne vivante partageant le même fichier.
« root » portait la racine de tous les filestores au lieu du dossier de
la base : le rangement cherchait à <data_dir>/filestore/filestore et
répondait « rien à ranger » devant 1168 fichiers échoués. Il en trouve
112 à remonter et 1056 doublons.
limit=0 veut dire « tout » partout ailleurs, mais [:0] est vide : « Tout
afficher » annonçait « … 3 de plus » sans en montrer un seul.
Le rapport n'était lu qu'une fois : après une purge il rejouait l'état
d'avant, et l'on repurgeait des lignes déjà effacées. Une réparation dit
maintenant si elle a fait quelque chose, et le rapport est relu alors.
« DELETE 0 » passait pour un succès. Le compte vient de ce que
PostgreSQL a annoncé, et ne rien dire n'est pas ne rien supprimer.
Assisted-by: Claude Opus 5
2026-08-21 23:20:08 -04:00
|
|
|
|
return False
|
[ADD] filestore: say if the record still exists, and offer the cleanups
A lost image on a deleted task is not a loss -- nobody will ever look
for it. On a LIVING task it is one, and it is the only one worth
regretting. The report now says which: project.task #15 and
calendar.event #1 both still exist, so those two really are gone.
Three states, not two: "could not check" must not read as "it is gone",
or a real loss gets filed as a false alarm.
Two repairs sit in the follow-up menu. Purging rows whose field no
longer exists deletes by ID, never by a rebuilt domain -- replaying the
reasoning in SQL would open the door to deleting more than was shown.
Tidying the nested filestore moves up what is missing and deletes pure
duplicates, never overwriting a file already in place.
--- FR ---
Une image perdue sur une tâche supprimée n'est pas une perte : personne
ne la cherchera. Sur une tâche VIVANTE, c'en est une, et la seule à
regretter. Le rapport le dit : project.task #15 et calendar.event #1
existent encore, ces deux-là sont bien perdues.
Trois états, pas deux : « pas pu vérifier » ne doit pas se lire « a
disparu », sans quoi une vraie perte passe pour une fausse alerte.
Deux réparations dans le menu de suite. La purge efface par IDENTIFIANT,
jamais par un domaine reconstruit -- rejouer le raisonnement en SQL
ouvrirait la porte à effacer plus que ce qui a été montré. Le rangement
remonte ce qui manque et supprime les doublons purs, sans jamais
écraser un fichier déjà en place.
Assisted-by: Claude Opus 5
2026-08-21 22:39:48 -04:00
|
|
|
|
deplaces, effaces = 0, 0
|
|
|
|
|
|
for source, cible in remonter:
|
|
|
|
|
|
os.makedirs(os.path.dirname(cible), exist_ok=True)
|
|
|
|
|
|
shutil.move(source, cible)
|
|
|
|
|
|
deplaces += 1
|
|
|
|
|
|
for source, _cible in doublons:
|
|
|
|
|
|
os.remove(source)
|
|
|
|
|
|
effaces += 1
|
|
|
|
|
|
dossier = filestore.nested_dir(rapport)
|
|
|
|
|
|
if dossier:
|
|
|
|
|
|
shutil.rmtree(dossier, ignore_errors=True)
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"✅ {deplaces} {t('moved up')}, {effaces}"
|
|
|
|
|
|
f" {t('duplicate(s) removed')}."
|
|
|
|
|
|
)
|
[FIX] filestore: five defects a real repair session brought out
Deduplicating by file was right for COUNTING and wrong for DELETING:
twenty-two rows shared two files, so the purge only ever offered one at
a time and had to be replayed. It now gathers every row whose own field
is dead, and never a live row sharing the same file.
"root" held the root of all filestores instead of this database's
directory, so tidying looked for a nested folder at
<data_dir>/filestore/filestore and answered "nothing to tidy" in front
of 1168 stranded files. It now finds 112 to move up and 1056 duplicates.
limit=0 means "no cap" everywhere else, but the slice [:0] is empty:
"Show every entry" printed "… 3 more" and showed none of them.
The report was captured once, so after a purge it replayed the state
from before -- one then purged rows already gone, believing the work
unfinished. A repair now says whether it did something, and the report
is re-read when it did.
"DELETE 0" was announced as a success. The count now comes from what
PostgreSQL said, and saying nothing is not the same as deleting nothing.
--- FR ---
Dédupliquer par fichier était juste pour COMPTER et faux pour EFFACER :
vingt-deux lignes partageaient deux fichiers, la purge n'en offrait
qu'une à la fois. Elle prend maintenant toutes les lignes dont le champ
est mort, et jamais une ligne vivante partageant le même fichier.
« root » portait la racine de tous les filestores au lieu du dossier de
la base : le rangement cherchait à <data_dir>/filestore/filestore et
répondait « rien à ranger » devant 1168 fichiers échoués. Il en trouve
112 à remonter et 1056 doublons.
limit=0 veut dire « tout » partout ailleurs, mais [:0] est vide : « Tout
afficher » annonçait « … 3 de plus » sans en montrer un seul.
Le rapport n'était lu qu'une fois : après une purge il rejouait l'état
d'avant, et l'on repurgeait des lignes déjà effacées. Une réparation dit
maintenant si elle a fait quelque chose, et le rapport est relu alors.
« DELETE 0 » passait pour un succès. Le compte vient de ce que
PostgreSQL a annoncé, et ne rien dire n'est pas ne rien supprimer.
Assisted-by: Claude Opus 5
2026-08-21 23:20:08 -04:00
|
|
|
|
return True
|
[ADD] filestore: say if the record still exists, and offer the cleanups
A lost image on a deleted task is not a loss -- nobody will ever look
for it. On a LIVING task it is one, and it is the only one worth
regretting. The report now says which: project.task #15 and
calendar.event #1 both still exist, so those two really are gone.
Three states, not two: "could not check" must not read as "it is gone",
or a real loss gets filed as a false alarm.
Two repairs sit in the follow-up menu. Purging rows whose field no
longer exists deletes by ID, never by a rebuilt domain -- replaying the
reasoning in SQL would open the door to deleting more than was shown.
Tidying the nested filestore moves up what is missing and deletes pure
duplicates, never overwriting a file already in place.
--- FR ---
Une image perdue sur une tâche supprimée n'est pas une perte : personne
ne la cherchera. Sur une tâche VIVANTE, c'en est une, et la seule à
regretter. Le rapport le dit : project.task #15 et calendar.event #1
existent encore, ces deux-là sont bien perdues.
Trois états, pas deux : « pas pu vérifier » ne doit pas se lire « a
disparu », sans quoi une vraie perte passe pour une fausse alerte.
Deux réparations dans le menu de suite. La purge efface par IDENTIFIANT,
jamais par un domaine reconstruit -- rejouer le raisonnement en SQL
ouvrirait la porte à effacer plus que ce qui a été montré. Le rangement
remonte ce qui manque et supprime les doublons purs, sans jamais
écraser un fichier déjà en place.
Assisted-by: Claude Opus 5
2026-08-21 22:39:48 -04:00
|
|
|
|
|
[ADD] analyse: offer to install the suggested modules that are ready
After the report, only the "available" ones are offered — an unknown
module is not in the addons path and an uninstallable one has a broken
dependency, so listing them would buy three failures. How many were
left out is stated, else the count would look like a bug.
This is the only write in the Analyse menu, so its header no longer
claims otherwise. Three guards: the checkout must match the database
version (an Odoo 18 run against a 12 rewrites it before failing), both
questions default to no, and a rejected token is always shown.
--- FR ---
Après le rapport, seuls les « available » sont proposés — un module
inconnu n'est pas dans le chemin des addons, un cassé a une dépendance
morte : les lister achèterait trois échecs. Le nombre d'écartés est dit,
sinon l'écart de comptage passerait pour un bogue.
C'est la seule écriture du menu Analyse, dont l'en-tête ne prétend donc
plus le contraire. Trois garde-fous : le checkout doit être sur la
version de la base (un Odoo 18 lancé sur une 12 la réécrit avant
d'échouer), les deux questions valent non par défaut, et un jeton
refusé est toujours montré.
Assisted-by: Claude Opus 5
2026-08-21 04:31:36 -04:00
|
|
|
|
def _analyse_offer_install(self, database, rapport):
|
|
|
|
|
|
"""Proposer d'installer ce qui manque, quand c'est installable.
|
|
|
|
|
|
|
|
|
|
|
|
Seuls les modules « available » sont offerts. Le dire est
|
|
|
|
|
|
nécessaire : le rapport vient d'en annoncer onze, la liste n'en
|
|
|
|
|
|
montre qu'un, et sans un mot on croirait à un bogue.
|
|
|
|
|
|
|
|
|
|
|
|
C'est la seule écriture de tout le menu Analyse, d'où trois
|
|
|
|
|
|
garde-fous : la version du checkout doit être celle de la base —
|
|
|
|
|
|
un Odoo 18 lancé sur une base 12 la réécrit avant d'échouer —, la
|
|
|
|
|
|
question par défaut est « non », et la liste choisie est
|
|
|
|
|
|
confirmée avant que rien ne parte.
|
|
|
|
|
|
"""
|
|
|
|
|
|
from script.analyse import check_module_package as modules
|
|
|
|
|
|
from script.odoo.migration import database_cleanup
|
|
|
|
|
|
from script.todo import auto_ask
|
|
|
|
|
|
|
|
|
|
|
|
candidats = modules.installable(rapport)
|
|
|
|
|
|
if not candidats:
|
|
|
|
|
|
return
|
|
|
|
|
|
autres = len(modules.missing(rapport)) - len(candidats)
|
|
|
|
|
|
|
|
|
|
|
|
souci = database_cleanup.require_matching_version(database)
|
|
|
|
|
|
if souci:
|
|
|
|
|
|
print(f"\n⚠ {souci}")
|
|
|
|
|
|
print(f" {t('Cannot install from here.')}")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
|
detail = f" ({autres} {t('need repair first')})" if autres else ""
|
|
|
|
|
|
question = (
|
|
|
|
|
|
f"💬 {t('Install some of the')} {len(candidats)}"
|
|
|
|
|
|
f" {t('suggested module(s) waiting in this database?')}{detail}"
|
|
|
|
|
|
f" (y/N) : "
|
|
|
|
|
|
)
|
|
|
|
|
|
if auto_ask.ask(question, default="n").strip().lower() not in (
|
|
|
|
|
|
"y",
|
|
|
|
|
|
"yes",
|
|
|
|
|
|
"o",
|
|
|
|
|
|
):
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
|
for rang, nom in enumerate(candidats, start=1):
|
|
|
|
|
|
print(f" [{rang}] {nom}")
|
|
|
|
|
|
print(f" [a] {t('every one of them')}")
|
|
|
|
|
|
print(f" {t('Enter = cancel')}")
|
|
|
|
|
|
choisis, refuses = modules.parse_selection(
|
|
|
|
|
|
auto_ask.ask(f"💬 {t('Numbers, space separated:')} ", default=""),
|
|
|
|
|
|
candidats,
|
|
|
|
|
|
)
|
|
|
|
|
|
# Un jeton refusé n'est JAMAIS avalé : en demander cinq et en
|
|
|
|
|
|
# recevoir quatre sans un mot ferait croire l'installation faite.
|
|
|
|
|
|
if refuses:
|
|
|
|
|
|
print(f"⚠ {t('Ignored, not in the list:')} {' '.join(refuses)}")
|
|
|
|
|
|
if not choisis:
|
|
|
|
|
|
print(f"ℹ️ {t('Nothing selected.')}")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(f" {t('About to install into')} {database} :")
|
|
|
|
|
|
print(f" {', '.join(choisis)}")
|
|
|
|
|
|
if auto_ask.ask(
|
|
|
|
|
|
f"💬 {t('Go ahead?')} (y/N) : ", default="n"
|
|
|
|
|
|
).strip().lower() not in ("y", "yes", "o"):
|
|
|
|
|
|
print(f"ℹ️ {t('Nothing selected.')}")
|
|
|
|
|
|
return
|
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
|
f"./script/addons/install_addons.sh {database}"
|
|
|
|
|
|
f" {','.join(choisis)}",
|
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
single_source_erplibre=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
[ADD] analyse: what a migration gained and lost, step by step
A migration leaves one database per step and they all still exist, so the
tool compares them side by side rather than replaying anything. Six Odoo
starts would cost an hour, write to the databases and need the checkout
switched at each step; the same inspection in SQL takes under half a
second per database and touches nothing. Measured on a real migration:
seven databases surveyed in 3.8 seconds.
It reports what each step gained and lost — modules, models, views, menus,
attachments — and ends on the comparison of the ENDS, which is not the sum
of the steps: a module dropped at 15 and put back at 17 lost nothing, and
adding the steps would count it twice.
The signal that matters is rows. A module removed is visible; a table
going from four thousand rows to zero is visible nowhere.
Two rename heuristics were tried and rejected on real data before the one
that holds. Row count alone paired an account tag table with dms_directory
— both had seven rows. A shared five-letter word paired two cleanup
tables. Overall name similarity separates them. And a renamed table stays
in the loss list, annotated: removing it was the real danger, since a
wrong pairing would have hidden a genuine loss.
--- FR ---
[ADD] analyse : ce qu'une migration a gagné et perdu, palier par palier
Une migration laisse une base par palier et elles existent toutes encore :
l'outil les compare côte à côte plutôt que de rejouer quoi que ce soit.
Six démarrages d'Odoo coûteraient une heure et écriraient dans les bases ;
la même inspection en SQL prend moins d'une demi-seconde par base. Mesuré
sur une vraie migration : sept bases inspectées en 3,8 secondes.
Le bilan final compare les EXTRÉMITÉS, ce qui n'est pas la somme des
paliers : un module retiré en 15 puis remis en 17 n'a rien perdu.
Le signal qui compte, ce sont les lignes. Un module en moins se voit ; une
table qui passe de quatre mille lignes à zéro ne se voit nulle part.
Deux rapprochements de renommage ont été essayés et rejetés sur des
données réelles. Et une table renommée reste dans la liste des pertes,
annotée : l'en retirer était le vrai danger.
Assisted-by: Claude Opus 5
2026-08-19 08:21:12 -04:00
|
|
|
|
def execute_analyse_migration_quality(self):
|
|
|
|
|
|
"""Ce qu'une migration a gagné et perdu, palier par palier.
|
|
|
|
|
|
|
|
|
|
|
|
Appelé comme les autres analyses — même interpréteur, aucun
|
|
|
|
|
|
sous-processus — mais l'écran plein est ouvert par l'outil
|
|
|
|
|
|
lui-même, qui sait retomber sur son rapport texte s'il ne peut pas.
|
|
|
|
|
|
|
|
|
|
|
|
Lecture seule de bout en bout : les bases de palier sont parfois la
|
|
|
|
|
|
seule copie qui reste d'un état intermédiaire.
|
|
|
|
|
|
"""
|
|
|
|
|
|
from script.analyse import check_migration_quality as quality
|
|
|
|
|
|
|
|
|
|
|
|
dct = quality.read_progression()
|
|
|
|
|
|
if not quality.chain(dct):
|
|
|
|
|
|
print(f"\nℹ️ {t('No migration in progress.')}")
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
lst = quality.survey(
|
|
|
|
|
|
dct, echo=lambda texte: print(f"⧖ {texte}", flush=True)
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
print(f"❌ {t('Analysis failed: ')}{exc}")
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
from script.analyse.check_migration_quality_tui import run_tui
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
run_tui = None
|
|
|
|
|
|
if not (run_tui and run_tui(lst)):
|
|
|
|
|
|
print(quality.render_text(lst))
|
|
|
|
|
|
|
[ADD] analyse: inspect an Odoo database without restoring it
A read-only toolkit answering « what is in this database, and what will
break on a version bump »: schema size and tables no model claims,
customised views compared with their module source, and the x_ fields
Studio left behind. It reads a backup zip directly, so a 40 GB dump needs
no restore, and it reaches every answer from the TODO menu.
--- FR ---
Une boîte à outils en lecture seule qui répond à « qu'y a-t-il dans cette
base, et qu'est-ce qui cassera à la montée de version » : taille du schéma
et tables qu'aucun modèle ne réclame, vues personnalisées comparées à leur
source, champs x_ laissés par Studio. Elle lit un zip de sauvegarde tel
quel, donc sans restaurer, et tout s'atteint depuis le menu TODO.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
def _analyse_select_source(self):
|
|
|
|
|
|
"""(est_une_sauvegarde, cible), ou None si l'on renonce.
|
|
|
|
|
|
|
|
|
|
|
|
La sauvegarde n'est pas un cas dégradé : restaurer celle d'une
|
|
|
|
|
|
instance Enterprise sur une installation Community échoue — Odoo veut
|
|
|
|
|
|
charger des modules qu'on n'a pas — donc c'est souvent la SEULE façon
|
|
|
|
|
|
de lire ce qu'elle contient.
|
|
|
|
|
|
"""
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(f"[1] {t('A database')}")
|
|
|
|
|
|
print(f"[2] {t('A backup .zip, without restoring it')}")
|
|
|
|
|
|
print(f"[0] {t('Back')}")
|
[FIX] todo : une invite n'écrit plus son deux-points deux fois
`click.prompt` ajoute son propre « : » à l'étiquette qu'on lui donne, et
quinze étiquettes le portaient déjà. Le menu le plus vu du logiciel demandait
donc « Commande :: », et sept invites de déploiement à distance affichaient
un deux-points suivi d'un autre.
Le suffixe est désormais explicite partout où l'étiquette ponctue elle-même,
et sa valeur se déduit de l'étiquette : rien quand elle porte déjà l'espace
qui suit, un espace quand le deux-points est nu. Un contrôle lit l'arbre et
résout les DEUX langues, le français mettant une espace avant le deux-points
là où l'anglais n'en met pas — chercher dans le texte du code ne verrait que
la clé anglaise.
--- EN ---
`click.prompt` adds its own ":" to the label it is given, and fifteen labels
already carried one. The most-seen menu of the software therefore asked
"Command:: ", and seven remote-deployment prompts showed a colon followed by
another.
The suffix is now explicit wherever the label punctuates itself, and its value
follows from the label: nothing when it already carries the trailing space, a
space when the colon is bare. A check reads the tree and resolves BOTH
languages, French putting a space before the colon where English does not —
searching the code text would only see the English key.
Assisted-by: Claude Opus 5
2026-09-09 05:32:52 -04:00
|
|
|
|
answer = click.prompt(t("Command:"), prompt_suffix=" ")
|
[ADD] analyse: inspect an Odoo database without restoring it
A read-only toolkit answering « what is in this database, and what will
break on a version bump »: schema size and tables no model claims,
customised views compared with their module source, and the x_ fields
Studio left behind. It reads a backup zip directly, so a 40 GB dump needs
no restore, and it reaches every answer from the TODO menu.
--- FR ---
Une boîte à outils en lecture seule qui répond à « qu'y a-t-il dans cette
base, et qu'est-ce qui cassera à la montée de version » : taille du schéma
et tables qu'aucun modèle ne réclame, vues personnalisées comparées à leur
source, champs x_ laissés par Studio. Elle lit un zip de sauvegarde tel
quel, donc sans restaurer, et tout s'atteint depuis le menu TODO.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
print()
|
|
|
|
|
|
if answer == "1":
|
|
|
|
|
|
database = self._analyse_select_database()
|
|
|
|
|
|
return (False, database) if database else None
|
|
|
|
|
|
if answer == "2":
|
|
|
|
|
|
path = self.db_manager.select_backup_path()
|
|
|
|
|
|
return (True, path) if path else None
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def _analyse_select_database(self):
|
|
|
|
|
|
"""Faire choisir la base à analyser, ou None si on abandonne."""
|
|
|
|
|
|
database = self.db_manager.select_database()
|
|
|
|
|
|
return database or None
|
|
|
|
|
|
|
|
|
|
|
|
def _analyse_json_path(self, database, tool):
|
|
|
|
|
|
"""Où écrire un export JSON. Le dossier est créé au besoin."""
|
|
|
|
|
|
directory = os.path.join("private", "analyse", database)
|
|
|
|
|
|
os.makedirs(directory, exist_ok=True)
|
|
|
|
|
|
return os.path.join(directory, f"{tool}.json")
|
|
|
|
|
|
|
|
|
|
|
|
def _analyse_export_json(self, data, database, tool):
|
|
|
|
|
|
"""Écrire le résultat brut, et dire où.
|
|
|
|
|
|
|
|
|
|
|
|
Sous `private/`, qui n'est pas versionné par convention : un rapport
|
|
|
|
|
|
d'analyse porte des noms de vues, de champs et de sociétés du client.
|
|
|
|
|
|
"""
|
|
|
|
|
|
path = self._analyse_json_path(database, tool)
|
|
|
|
|
|
with open(path, "w", encoding="utf-8") as handle:
|
|
|
|
|
|
json.dump(data, handle, indent=2, ensure_ascii=False, default=str)
|
|
|
|
|
|
print(f"✅ {t('Written to: ')}{path}")
|
|
|
|
|
|
|
|
|
|
|
|
def _analyse_follow_up(self, choices, handler):
|
|
|
|
|
|
"""Boucle « aller plus loin » après une analyse.
|
|
|
|
|
|
|
|
|
|
|
|
Le rapport suggérait « utilisez -v », « --exact », « ajoutez --diff ».
|
|
|
|
|
|
Dans un menu, c'est demander à l'utilisateur de sortir et de retaper
|
|
|
|
|
|
une commande pour obtenir ce que le menu pouvait lui offrir. Les
|
|
|
|
|
|
options sont donc devenues des entrées, et les conseils en ligne de
|
|
|
|
|
|
commande ne s'affichent plus que dans la vraie ligne de commande.
|
|
|
|
|
|
"""
|
|
|
|
|
|
help_info = self.fill_help_info(
|
|
|
|
|
|
[{"section": t("Go further")}] + choices
|
|
|
|
|
|
)
|
|
|
|
|
|
while True:
|
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
|
print()
|
|
|
|
|
|
if status == "0":
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
rank = int(status)
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
rank = 0
|
|
|
|
|
|
if not 1 <= rank <= len(choices):
|
|
|
|
|
|
print(t("Command not found !"))
|
|
|
|
|
|
continue
|
|
|
|
|
|
if handler(rank) is False:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
def execute_analyse_schema_size(self):
|
|
|
|
|
|
"""Poids de la base et tables qu'aucun modèle installé ne réclame.
|
|
|
|
|
|
|
|
|
|
|
|
L'outil est importé et appelé, pas lancé en sous-processus : `todo.py`
|
|
|
|
|
|
tourne déjà sous le même interpréteur, donc le sous-processus
|
|
|
|
|
|
n'apporterait aucun isolement et coûterait un second démarrage.
|
|
|
|
|
|
|
|
|
|
|
|
Contrepartie assumée de cet appel direct : une exception remonterait
|
|
|
|
|
|
dans la boucle du menu et ferait sortir du TODO. D'où le `try`.
|
|
|
|
|
|
"""
|
|
|
|
|
|
from script.analyse import analyse_schema_size as analyse
|
|
|
|
|
|
|
|
|
|
|
|
target = self._analyse_select_source()
|
|
|
|
|
|
if not target:
|
|
|
|
|
|
return
|
|
|
|
|
|
is_backup, database = target
|
|
|
|
|
|
state = {"data": None, "exact": is_backup}
|
|
|
|
|
|
|
|
|
|
|
|
def run(exact=False):
|
|
|
|
|
|
try:
|
|
|
|
|
|
state["data"] = (
|
|
|
|
|
|
analyse.collect_from_backup(database)
|
|
|
|
|
|
if is_backup
|
|
|
|
|
|
else analyse.collect(database, exact=exact)
|
|
|
|
|
|
)
|
|
|
|
|
|
state["exact"] = exact or is_backup
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
print(f"❌ {t('Analysis failed: ')}{exc}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
if not run():
|
|
|
|
|
|
return
|
|
|
|
|
|
print(analyse.render(state["data"], hints=False))
|
|
|
|
|
|
|
|
|
|
|
|
def handler(rank):
|
|
|
|
|
|
data = state["data"]
|
|
|
|
|
|
if rank == 1:
|
|
|
|
|
|
print(analyse.render(data, verbose=True, hints=False))
|
|
|
|
|
|
elif rank == 2:
|
|
|
|
|
|
print(f"⏳ {t('Counting rows exactly, one scan per table…')}")
|
|
|
|
|
|
if run(exact=True):
|
|
|
|
|
|
print(analyse.render(state["data"], hints=False))
|
|
|
|
|
|
else:
|
|
|
|
|
|
self._analyse_export_json(
|
|
|
|
|
|
data, os.path.basename(database), "schema_size"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
self._analyse_follow_up(
|
|
|
|
|
|
[
|
|
|
|
|
|
{"prompt_description": t("Show every table")},
|
|
|
|
|
|
{"prompt_description": t("Count rows exactly (full scan)")},
|
|
|
|
|
|
{"prompt_description": t("Export as JSON")},
|
|
|
|
|
|
],
|
|
|
|
|
|
handler,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def execute_analyse_view_custom(self):
|
|
|
|
|
|
"""Vues qui ne viennent pas telles quelles d'un module, COW comprises."""
|
|
|
|
|
|
from script.analyse import analyse_view_custom as analyse
|
|
|
|
|
|
|
|
|
|
|
|
target = self._analyse_select_source()
|
|
|
|
|
|
if not target:
|
|
|
|
|
|
return
|
|
|
|
|
|
is_backup, database = target
|
|
|
|
|
|
state = {"data": None}
|
|
|
|
|
|
|
|
|
|
|
|
def run(**kwargs):
|
|
|
|
|
|
try:
|
[ADD] analyse: diff each website copy against the view it shadows
The analysis counted 62 website copies and pointed at other tools to judge
them. But the comparison that matters for a copy needs no registry at all:
both arches are in the database, paired by key — the very pairing Odoo makes.
So it works where the module-source comparison is refused, on a database whose
version differs from the checkout, and on a backup zip.
On the 12.0 database at hand, where the other comparison cannot run: 47 of the
62 copies have a twin, all 47 compared in under a second, 12 differ. The other
35 are byte-for-byte identical to their module view — they carry no
customization at all, which is what decides whether neutralizing costs
anything.
The fields are named as the module comparison names them, so the full-screen
browser and the text report work without knowing which comparison produced the
data. A copy without a twin is left alone: it is a page made in the editor,
with nothing to compare against.
Checked on that database and on a real backup; 8 tests, including that
re-indentation alone is not a difference and that a missing arch yields no
verdict rather than « identical ».
--- FR ---
L'analyse comptait 62 copies de site web et renvoyait vers d'autres outils
pour les juger. Or la comparaison qui compte pour une copie n'a besoin d'aucun
registre : les deux arch sont dans la base, appariées par la clé — exactement
l'appariement que fait Odoo. Elle marche donc là où celle avec la source du
module est refusée : une base dont la version diffère du checkout, et une
sauvegarde zip.
Sur la base 12.0 en cours, où l'autre comparaison ne peut pas tourner : 47 des
62 copies ont une jumelle, les 47 comparées en moins d'une seconde, 12
diffèrent. Les 35 autres sont identiques octet pour octet à leur vue de module
— elles ne portent aucune personnalisation, ce qui décide si les neutraliser
coûte quelque chose.
Les champs portent les noms de la comparaison avec le module, donc l'écran de
navigation et le rapport texte marchent sans savoir laquelle des deux a
produit la donnée. Une copie sans jumelle est laissée telle quelle : c'est une
page faite dans l'éditeur, il n'y a rien à quoi la comparer.
Vérifié sur cette base et sur une vraie sauvegarde ; 8 tests, dont qu'une
ré-indentation seule n'est pas un écart et qu'une arch absente ne rend aucun
verdict plutôt que « identique ».
Assisted-by: Claude Opus 5
2026-08-12 00:54:11 -04:00
|
|
|
|
# Une sauvegarde compare déjà ses copies COW à la lecture :
|
|
|
|
|
|
# les deux arch sont dans le dump, il n'y a rien à demander.
|
[ADD] analyse: inspect an Odoo database without restoring it
A read-only toolkit answering « what is in this database, and what will
break on a version bump »: schema size and tables no model claims,
customised views compared with their module source, and the x_ fields
Studio left behind. It reads a backup zip directly, so a 40 GB dump needs
no restore, and it reaches every answer from the TODO menu.
--- FR ---
Une boîte à outils en lecture seule qui répond à « qu'y a-t-il dans cette
base, et qu'est-ce qui cassera à la montée de version » : taille du schéma
et tables qu'aucun modèle ne réclame, vues personnalisées comparées à leur
source, champs x_ laissés par Studio. Elle lit un zip de sauvegarde tel
quel, donc sans restaurer, et tout s'atteint depuis le menu TODO.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
state["data"] = (
|
|
|
|
|
|
analyse.collect_from_backup(database)
|
|
|
|
|
|
if is_backup
|
|
|
|
|
|
else analyse.collect(database, **kwargs)
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
print(f"❌ {t('Analysis failed: ')}{exc}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
if not run():
|
|
|
|
|
|
return
|
|
|
|
|
|
print(analyse.render(state["data"], hints=False))
|
|
|
|
|
|
if not state["data"]["findings"]:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Comparer exige un registre Odoo chargé. Une sauvegarde n'en a pas,
|
|
|
|
|
|
# et rien ne peut l'y ajouter : proposer quand même la comparaison
|
|
|
|
|
|
# ferait trois entrées qui ne répondent pas, et une quatrième qui
|
|
|
|
|
|
# réclamerait indéfiniment une comparaison impossible.
|
|
|
|
|
|
can_compare = not is_backup
|
|
|
|
|
|
state["tried"] = False
|
|
|
|
|
|
|
|
|
|
|
|
def compare(scope):
|
|
|
|
|
|
print(f"⏳ {t('Loading the Odoo registry, this takes a moment…')}")
|
|
|
|
|
|
if not run(with_diff=True, scope=scope):
|
|
|
|
|
|
return
|
|
|
|
|
|
state["tried"] = True
|
|
|
|
|
|
data = state["data"]
|
|
|
|
|
|
print(analyse.render(data, hints=False))
|
|
|
|
|
|
if data["compared_with_module_source"] and not [
|
|
|
|
|
|
row for row in data["findings"] if row.get("differs")
|
|
|
|
|
|
]:
|
|
|
|
|
|
print(f"✅ {t('No view differs from its module source.')}")
|
|
|
|
|
|
|
|
|
|
|
|
def browse():
|
|
|
|
|
|
"""Ouvrir l'écran, ou dire précisément ce qui l'en empêche.
|
|
|
|
|
|
|
|
|
|
|
|
Trois raisons distinctes, trois messages. Répondre « comparez
|
|
|
|
|
|
d'abord » à quelqu'un qui vient de comparer lui reproche ce que
|
|
|
|
|
|
l'outil n'a pas pu faire, et le laisse recommencer sans fin.
|
|
|
|
|
|
"""
|
|
|
|
|
|
data = state["data"]
|
|
|
|
|
|
if not state["tried"]:
|
|
|
|
|
|
print(f"ℹ️ {t('Compare first, then browse.')}")
|
|
|
|
|
|
elif not data.get("compared_with_module_source"):
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"⚠️ {t('No reference arch, so nothing was compared: ')}"
|
|
|
|
|
|
f"{data.get('arch_ref_error') or ''}"
|
|
|
|
|
|
)
|
|
|
|
|
|
elif not [row for row in data["findings"] if row.get("differs")]:
|
|
|
|
|
|
print(f"✅ {t('No view differs from its module source.')}")
|
|
|
|
|
|
elif not analyse.open_tui(data):
|
|
|
|
|
|
print(analyse.render(data, verbose=True, hints=False))
|
|
|
|
|
|
|
|
|
|
|
|
lst_choice = [{"prompt_description": t("Show every view")}]
|
[ADD] analyse: diff each website copy against the view it shadows
The analysis counted 62 website copies and pointed at other tools to judge
them. But the comparison that matters for a copy needs no registry at all:
both arches are in the database, paired by key — the very pairing Odoo makes.
So it works where the module-source comparison is refused, on a database whose
version differs from the checkout, and on a backup zip.
On the 12.0 database at hand, where the other comparison cannot run: 47 of the
62 copies have a twin, all 47 compared in under a second, 12 differ. The other
35 are byte-for-byte identical to their module view — they carry no
customization at all, which is what decides whether neutralizing costs
anything.
The fields are named as the module comparison names them, so the full-screen
browser and the text report work without knowing which comparison produced the
data. A copy without a twin is left alone: it is a page made in the editor,
with nothing to compare against.
Checked on that database and on a real backup; 8 tests, including that
re-indentation alone is not a difference and that a missing arch yields no
verdict rather than « identical ».
--- FR ---
L'analyse comptait 62 copies de site web et renvoyait vers d'autres outils
pour les juger. Or la comparaison qui compte pour une copie n'a besoin d'aucun
registre : les deux arch sont dans la base, appariées par la clé — exactement
l'appariement que fait Odoo. Elle marche donc là où celle avec la source du
module est refusée : une base dont la version diffère du checkout, et une
sauvegarde zip.
Sur la base 12.0 en cours, où l'autre comparaison ne peut pas tourner : 47 des
62 copies ont une jumelle, les 47 comparées en moins d'une seconde, 12
diffèrent. Les 35 autres sont identiques octet pour octet à leur vue de module
— elles ne portent aucune personnalisation, ce qui décide si les neutraliser
coûte quelque chose.
Les champs portent les noms de la comparaison avec le module, donc l'écran de
navigation et le rapport texte marchent sans savoir laquelle des deux a
produit la donnée. Une copie sans jumelle est laissée telle quelle : c'est une
page faite dans l'éditeur, il n'y a rien à quoi la comparer.
Vérifié sur cette base et sur une vraie sauvegarde ; 8 tests, dont qu'une
ré-indentation seule n'est pas un écart et qu'une arch absente ne rend aucun
verdict plutôt que « identique ».
Assisted-by: Claude Opus 5
2026-08-12 00:54:11 -04:00
|
|
|
|
# La comparaison des copies COW n'a besoin d'aucun registre : les deux
|
|
|
|
|
|
# arch sont dans la base, appariées par leur clé. Elle est donc offerte
|
|
|
|
|
|
# partout, y compris sur une sauvegarde et sur une base dont la version
|
|
|
|
|
|
# diffère du checkout — là où l'autre comparaison est refusée.
|
|
|
|
|
|
has_cow = bool(state["data"]["counts"].get("website_cow_copy"))
|
|
|
|
|
|
if has_cow:
|
|
|
|
|
|
lst_choice.append(
|
|
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
|
"Compare the website copies with the view they shadow"
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
[ADD] analyse: inspect an Odoo database without restoring it
A read-only toolkit answering « what is in this database, and what will
break on a version bump »: schema size and tables no model claims,
customised views compared with their module source, and the x_ fields
Studio left behind. It reads a backup zip directly, so a 40 GB dump needs
no restore, and it reaches every answer from the TODO menu.
--- FR ---
Une boîte à outils en lecture seule qui répond à « qu'y a-t-il dans cette
base, et qu'est-ce qui cassera à la montée de version » : taille du schéma
et tables qu'aucun modèle ne réclame, vues personnalisées comparées à leur
source, champs x_ laissés par Studio. Elle lit un zip de sauvegarde tel
quel, donc sans restaurer, et tout s'atteint depuis le menu TODO.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
if can_compare:
|
|
|
|
|
|
lst_choice += [
|
|
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
|
"Compare the flagged views with the module source"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
|
"Compare every view (slower, noisier)"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
|
|
|
|
|
{"prompt_description": t("Browse the differences (TUI)")},
|
|
|
|
|
|
]
|
|
|
|
|
|
lst_choice.append({"prompt_description": t("Export as JSON")})
|
|
|
|
|
|
|
[ADD] analyse: diff each website copy against the view it shadows
The analysis counted 62 website copies and pointed at other tools to judge
them. But the comparison that matters for a copy needs no registry at all:
both arches are in the database, paired by key — the very pairing Odoo makes.
So it works where the module-source comparison is refused, on a database whose
version differs from the checkout, and on a backup zip.
On the 12.0 database at hand, where the other comparison cannot run: 47 of the
62 copies have a twin, all 47 compared in under a second, 12 differ. The other
35 are byte-for-byte identical to their module view — they carry no
customization at all, which is what decides whether neutralizing costs
anything.
The fields are named as the module comparison names them, so the full-screen
browser and the text report work without knowing which comparison produced the
data. A copy without a twin is left alone: it is a page made in the editor,
with nothing to compare against.
Checked on that database and on a real backup; 8 tests, including that
re-indentation alone is not a difference and that a missing arch yields no
verdict rather than « identical ».
--- FR ---
L'analyse comptait 62 copies de site web et renvoyait vers d'autres outils
pour les juger. Or la comparaison qui compte pour une copie n'a besoin d'aucun
registre : les deux arch sont dans la base, appariées par la clé — exactement
l'appariement que fait Odoo. Elle marche donc là où celle avec la source du
module est refusée : une base dont la version diffère du checkout, et une
sauvegarde zip.
Sur la base 12.0 en cours, où l'autre comparaison ne peut pas tourner : 47 des
62 copies ont une jumelle, les 47 comparées en moins d'une seconde, 12
diffèrent. Les 35 autres sont identiques octet pour octet à leur vue de module
— elles ne portent aucune personnalisation, ce qui décide si les neutraliser
coûte quelque chose.
Les champs portent les noms de la comparaison avec le module, donc l'écran de
navigation et le rapport texte marchent sans savoir laquelle des deux a
produit la donnée. Une copie sans jumelle est laissée telle quelle : c'est une
page faite dans l'éditeur, il n'y a rien à quoi la comparer.
Vérifié sur cette base et sur une vraie sauvegarde ; 8 tests, dont qu'une
ré-indentation seule n'est pas un écart et qu'une arch absente ne rend aucun
verdict plutôt que « identique ».
Assisted-by: Claude Opus 5
2026-08-12 00:54:11 -04:00
|
|
|
|
def compare_cow():
|
|
|
|
|
|
if not run(with_cow_diff=True):
|
|
|
|
|
|
return
|
|
|
|
|
|
state["tried"] = True
|
|
|
|
|
|
data = state["data"]
|
|
|
|
|
|
print(analyse.render(data, hints=False))
|
|
|
|
|
|
n = data.get("n_cow_compared") or 0
|
|
|
|
|
|
n_diff = len([r for r in data["findings"] if r.get("differs")])
|
|
|
|
|
|
print(
|
|
|
|
|
|
f" {n} {t('website copies compared with their module view,')}"
|
|
|
|
|
|
f" {n_diff} {t('differ.')}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
[ADD] analyse: inspect an Odoo database without restoring it
A read-only toolkit answering « what is in this database, and what will
break on a version bump »: schema size and tables no model claims,
customised views compared with their module source, and the x_ fields
Studio left behind. It reads a backup zip directly, so a 40 GB dump needs
no restore, and it reaches every answer from the TODO menu.
--- FR ---
Une boîte à outils en lecture seule qui répond à « qu'y a-t-il dans cette
base, et qu'est-ce qui cassera à la montée de version » : taille du schéma
et tables qu'aucun modèle ne réclame, vues personnalisées comparées à leur
source, champs x_ laissés par Studio. Elle lit un zip de sauvegarde tel
quel, donc sans restaurer, et tout s'atteint depuis le menu TODO.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
def handler(rank):
|
|
|
|
|
|
data = state["data"]
|
[ADD] analyse: diff each website copy against the view it shadows
The analysis counted 62 website copies and pointed at other tools to judge
them. But the comparison that matters for a copy needs no registry at all:
both arches are in the database, paired by key — the very pairing Odoo makes.
So it works where the module-source comparison is refused, on a database whose
version differs from the checkout, and on a backup zip.
On the 12.0 database at hand, where the other comparison cannot run: 47 of the
62 copies have a twin, all 47 compared in under a second, 12 differ. The other
35 are byte-for-byte identical to their module view — they carry no
customization at all, which is what decides whether neutralizing costs
anything.
The fields are named as the module comparison names them, so the full-screen
browser and the text report work without knowing which comparison produced the
data. A copy without a twin is left alone: it is a page made in the editor,
with nothing to compare against.
Checked on that database and on a real backup; 8 tests, including that
re-indentation alone is not a difference and that a missing arch yields no
verdict rather than « identical ».
--- FR ---
L'analyse comptait 62 copies de site web et renvoyait vers d'autres outils
pour les juger. Or la comparaison qui compte pour une copie n'a besoin d'aucun
registre : les deux arch sont dans la base, appariées par la clé — exactement
l'appariement que fait Odoo. Elle marche donc là où celle avec la source du
module est refusée : une base dont la version diffère du checkout, et une
sauvegarde zip.
Sur la base 12.0 en cours, où l'autre comparaison ne peut pas tourner : 47 des
62 copies ont une jumelle, les 47 comparées en moins d'une seconde, 12
diffèrent. Les 35 autres sont identiques octet pour octet à leur vue de module
— elles ne portent aucune personnalisation, ce qui décide si les neutraliser
coûte quelque chose.
Les champs portent les noms de la comparaison avec le module, donc l'écran de
navigation et le rapport texte marchent sans savoir laquelle des deux a
produit la donnée. Une copie sans jumelle est laissée telle quelle : c'est une
page faite dans l'éditeur, il n'y a rien à quoi la comparer.
Vérifié sur cette base et sur une vraie sauvegarde ; 8 tests, dont qu'une
ré-indentation seule n'est pas un écart et qu'une arch absente ne rend aucun
verdict plutôt que « identique ».
Assisted-by: Claude Opus 5
2026-08-12 00:54:11 -04:00
|
|
|
|
offset = 1 if has_cow else 0
|
[ADD] analyse: inspect an Odoo database without restoring it
A read-only toolkit answering « what is in this database, and what will
break on a version bump »: schema size and tables no model claims,
customised views compared with their module source, and the x_ fields
Studio left behind. It reads a backup zip directly, so a 40 GB dump needs
no restore, and it reaches every answer from the TODO menu.
--- FR ---
Une boîte à outils en lecture seule qui répond à « qu'y a-t-il dans cette
base, et qu'est-ce qui cassera à la montée de version » : taille du schéma
et tables qu'aucun modèle ne réclame, vues personnalisées comparées à leur
source, champs x_ laissés par Studio. Elle lit un zip de sauvegarde tel
quel, donc sans restaurer, et tout s'atteint depuis le menu TODO.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
if rank == 1:
|
|
|
|
|
|
print(analyse.render(data, verbose=True, hints=False))
|
[ADD] analyse: diff each website copy against the view it shadows
The analysis counted 62 website copies and pointed at other tools to judge
them. But the comparison that matters for a copy needs no registry at all:
both arches are in the database, paired by key — the very pairing Odoo makes.
So it works where the module-source comparison is refused, on a database whose
version differs from the checkout, and on a backup zip.
On the 12.0 database at hand, where the other comparison cannot run: 47 of the
62 copies have a twin, all 47 compared in under a second, 12 differ. The other
35 are byte-for-byte identical to their module view — they carry no
customization at all, which is what decides whether neutralizing costs
anything.
The fields are named as the module comparison names them, so the full-screen
browser and the text report work without knowing which comparison produced the
data. A copy without a twin is left alone: it is a page made in the editor,
with nothing to compare against.
Checked on that database and on a real backup; 8 tests, including that
re-indentation alone is not a difference and that a missing arch yields no
verdict rather than « identical ».
--- FR ---
L'analyse comptait 62 copies de site web et renvoyait vers d'autres outils
pour les juger. Or la comparaison qui compte pour une copie n'a besoin d'aucun
registre : les deux arch sont dans la base, appariées par la clé — exactement
l'appariement que fait Odoo. Elle marche donc là où celle avec la source du
module est refusée : une base dont la version diffère du checkout, et une
sauvegarde zip.
Sur la base 12.0 en cours, où l'autre comparaison ne peut pas tourner : 47 des
62 copies ont une jumelle, les 47 comparées en moins d'une seconde, 12
diffèrent. Les 35 autres sont identiques octet pour octet à leur vue de module
— elles ne portent aucune personnalisation, ce qui décide si les neutraliser
coûte quelque chose.
Les champs portent les noms de la comparaison avec le module, donc l'écran de
navigation et le rapport texte marchent sans savoir laquelle des deux a
produit la donnée. Une copie sans jumelle est laissée telle quelle : c'est une
page faite dans l'éditeur, il n'y a rien à quoi la comparer.
Vérifié sur cette base et sur une vraie sauvegarde ; 8 tests, dont qu'une
ré-indentation seule n'est pas un écart et qu'une arch absente ne rend aucun
verdict plutôt que « identique ».
Assisted-by: Claude Opus 5
2026-08-12 00:54:11 -04:00
|
|
|
|
elif has_cow and rank == 2:
|
|
|
|
|
|
compare_cow()
|
|
|
|
|
|
elif rank == len(lst_choice):
|
[ADD] analyse: inspect an Odoo database without restoring it
A read-only toolkit answering « what is in this database, and what will
break on a version bump »: schema size and tables no model claims,
customised views compared with their module source, and the x_ fields
Studio left behind. It reads a backup zip directly, so a 40 GB dump needs
no restore, and it reaches every answer from the TODO menu.
--- FR ---
Une boîte à outils en lecture seule qui répond à « qu'y a-t-il dans cette
base, et qu'est-ce qui cassera à la montée de version » : taille du schéma
et tables qu'aucun modèle ne réclame, vues personnalisées comparées à leur
source, champs x_ laissés par Studio. Elle lit un zip de sauvegarde tel
quel, donc sans restaurer, et tout s'atteint depuis le menu TODO.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
self._analyse_export_json(
|
|
|
|
|
|
data, os.path.basename(database), "view_custom"
|
|
|
|
|
|
)
|
[ADD] analyse: diff each website copy against the view it shadows
The analysis counted 62 website copies and pointed at other tools to judge
them. But the comparison that matters for a copy needs no registry at all:
both arches are in the database, paired by key — the very pairing Odoo makes.
So it works where the module-source comparison is refused, on a database whose
version differs from the checkout, and on a backup zip.
On the 12.0 database at hand, where the other comparison cannot run: 47 of the
62 copies have a twin, all 47 compared in under a second, 12 differ. The other
35 are byte-for-byte identical to their module view — they carry no
customization at all, which is what decides whether neutralizing costs
anything.
The fields are named as the module comparison names them, so the full-screen
browser and the text report work without knowing which comparison produced the
data. A copy without a twin is left alone: it is a page made in the editor,
with nothing to compare against.
Checked on that database and on a real backup; 8 tests, including that
re-indentation alone is not a difference and that a missing arch yields no
verdict rather than « identical ».
--- FR ---
L'analyse comptait 62 copies de site web et renvoyait vers d'autres outils
pour les juger. Or la comparaison qui compte pour une copie n'a besoin d'aucun
registre : les deux arch sont dans la base, appariées par la clé — exactement
l'appariement que fait Odoo. Elle marche donc là où celle avec la source du
module est refusée : une base dont la version diffère du checkout, et une
sauvegarde zip.
Sur la base 12.0 en cours, où l'autre comparaison ne peut pas tourner : 47 des
62 copies ont une jumelle, les 47 comparées en moins d'une seconde, 12
diffèrent. Les 35 autres sont identiques octet pour octet à leur vue de module
— elles ne portent aucune personnalisation, ce qui décide si les neutraliser
coûte quelque chose.
Les champs portent les noms de la comparaison avec le module, donc l'écran de
navigation et le rapport texte marchent sans savoir laquelle des deux a
produit la donnée. Une copie sans jumelle est laissée telle quelle : c'est une
page faite dans l'éditeur, il n'y a rien à quoi la comparer.
Vérifié sur cette base et sur une vraie sauvegarde ; 8 tests, dont qu'une
ré-indentation seule n'est pas un écart et qu'une arch absente ne rend aucun
verdict plutôt que « identique ».
Assisted-by: Claude Opus 5
2026-08-12 00:54:11 -04:00
|
|
|
|
elif can_compare and rank == 2 + offset:
|
[ADD] analyse: inspect an Odoo database without restoring it
A read-only toolkit answering « what is in this database, and what will
break on a version bump »: schema size and tables no model claims,
customised views compared with their module source, and the x_ fields
Studio left behind. It reads a backup zip directly, so a 40 GB dump needs
no restore, and it reaches every answer from the TODO menu.
--- FR ---
Une boîte à outils en lecture seule qui répond à « qu'y a-t-il dans cette
base, et qu'est-ce qui cassera à la montée de version » : taille du schéma
et tables qu'aucun modèle ne réclame, vues personnalisées comparées à leur
source, champs x_ laissés par Studio. Elle lit un zip de sauvegarde tel
quel, donc sans restaurer, et tout s'atteint depuis le menu TODO.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
compare("flagged")
|
[ADD] analyse: diff each website copy against the view it shadows
The analysis counted 62 website copies and pointed at other tools to judge
them. But the comparison that matters for a copy needs no registry at all:
both arches are in the database, paired by key — the very pairing Odoo makes.
So it works where the module-source comparison is refused, on a database whose
version differs from the checkout, and on a backup zip.
On the 12.0 database at hand, where the other comparison cannot run: 47 of the
62 copies have a twin, all 47 compared in under a second, 12 differ. The other
35 are byte-for-byte identical to their module view — they carry no
customization at all, which is what decides whether neutralizing costs
anything.
The fields are named as the module comparison names them, so the full-screen
browser and the text report work without knowing which comparison produced the
data. A copy without a twin is left alone: it is a page made in the editor,
with nothing to compare against.
Checked on that database and on a real backup; 8 tests, including that
re-indentation alone is not a difference and that a missing arch yields no
verdict rather than « identical ».
--- FR ---
L'analyse comptait 62 copies de site web et renvoyait vers d'autres outils
pour les juger. Or la comparaison qui compte pour une copie n'a besoin d'aucun
registre : les deux arch sont dans la base, appariées par la clé — exactement
l'appariement que fait Odoo. Elle marche donc là où celle avec la source du
module est refusée : une base dont la version diffère du checkout, et une
sauvegarde zip.
Sur la base 12.0 en cours, où l'autre comparaison ne peut pas tourner : 47 des
62 copies ont une jumelle, les 47 comparées en moins d'une seconde, 12
diffèrent. Les 35 autres sont identiques octet pour octet à leur vue de module
— elles ne portent aucune personnalisation, ce qui décide si les neutraliser
coûte quelque chose.
Les champs portent les noms de la comparaison avec le module, donc l'écran de
navigation et le rapport texte marchent sans savoir laquelle des deux a
produit la donnée. Une copie sans jumelle est laissée telle quelle : c'est une
page faite dans l'éditeur, il n'y a rien à quoi la comparer.
Vérifié sur cette base et sur une vraie sauvegarde ; 8 tests, dont qu'une
ré-indentation seule n'est pas un écart et qu'une arch absente ne rend aucun
verdict plutôt que « identique ».
Assisted-by: Claude Opus 5
2026-08-12 00:54:11 -04:00
|
|
|
|
elif can_compare and rank == 3 + offset:
|
[ADD] analyse: inspect an Odoo database without restoring it
A read-only toolkit answering « what is in this database, and what will
break on a version bump »: schema size and tables no model claims,
customised views compared with their module source, and the x_ fields
Studio left behind. It reads a backup zip directly, so a 40 GB dump needs
no restore, and it reaches every answer from the TODO menu.
--- FR ---
Une boîte à outils en lecture seule qui répond à « qu'y a-t-il dans cette
base, et qu'est-ce qui cassera à la montée de version » : taille du schéma
et tables qu'aucun modèle ne réclame, vues personnalisées comparées à leur
source, champs x_ laissés par Studio. Elle lit un zip de sauvegarde tel
quel, donc sans restaurer, et tout s'atteint depuis le menu TODO.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
compare("all")
|
[ADD] analyse: diff each website copy against the view it shadows
The analysis counted 62 website copies and pointed at other tools to judge
them. But the comparison that matters for a copy needs no registry at all:
both arches are in the database, paired by key — the very pairing Odoo makes.
So it works where the module-source comparison is refused, on a database whose
version differs from the checkout, and on a backup zip.
On the 12.0 database at hand, where the other comparison cannot run: 47 of the
62 copies have a twin, all 47 compared in under a second, 12 differ. The other
35 are byte-for-byte identical to their module view — they carry no
customization at all, which is what decides whether neutralizing costs
anything.
The fields are named as the module comparison names them, so the full-screen
browser and the text report work without knowing which comparison produced the
data. A copy without a twin is left alone: it is a page made in the editor,
with nothing to compare against.
Checked on that database and on a real backup; 8 tests, including that
re-indentation alone is not a difference and that a missing arch yields no
verdict rather than « identical ».
--- FR ---
L'analyse comptait 62 copies de site web et renvoyait vers d'autres outils
pour les juger. Or la comparaison qui compte pour une copie n'a besoin d'aucun
registre : les deux arch sont dans la base, appariées par la clé — exactement
l'appariement que fait Odoo. Elle marche donc là où celle avec la source du
module est refusée : une base dont la version diffère du checkout, et une
sauvegarde zip.
Sur la base 12.0 en cours, où l'autre comparaison ne peut pas tourner : 47 des
62 copies ont une jumelle, les 47 comparées en moins d'une seconde, 12
diffèrent. Les 35 autres sont identiques octet pour octet à leur vue de module
— elles ne portent aucune personnalisation, ce qui décide si les neutraliser
coûte quelque chose.
Les champs portent les noms de la comparaison avec le module, donc l'écran de
navigation et le rapport texte marchent sans savoir laquelle des deux a
produit la donnée. Une copie sans jumelle est laissée telle quelle : c'est une
page faite dans l'éditeur, il n'y a rien à quoi la comparer.
Vérifié sur cette base et sur une vraie sauvegarde ; 8 tests, dont qu'une
ré-indentation seule n'est pas un écart et qu'une arch absente ne rend aucun
verdict plutôt que « identique ».
Assisted-by: Claude Opus 5
2026-08-12 00:54:11 -04:00
|
|
|
|
elif can_compare and rank == 4 + offset:
|
[ADD] analyse: inspect an Odoo database without restoring it
A read-only toolkit answering « what is in this database, and what will
break on a version bump »: schema size and tables no model claims,
customised views compared with their module source, and the x_ fields
Studio left behind. It reads a backup zip directly, so a 40 GB dump needs
no restore, and it reaches every answer from the TODO menu.
--- FR ---
Une boîte à outils en lecture seule qui répond à « qu'y a-t-il dans cette
base, et qu'est-ce qui cassera à la montée de version » : taille du schéma
et tables qu'aucun modèle ne réclame, vues personnalisées comparées à leur
source, champs x_ laissés par Studio. Elle lit un zip de sauvegarde tel
quel, donc sans restaurer, et tout s'atteint depuis le menu TODO.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
|
browse()
|
|
|
|
|
|
|
|
|
|
|
|
self._analyse_follow_up(lst_choice, handler)
|
|
|
|
|
|
|
|
|
|
|
|
def execute_analyse_custom_field(self):
|
|
|
|
|
|
"""Champs et modèles ajoutés hors module : Studio, ou faits à la main.
|
|
|
|
|
|
|
|
|
|
|
|
Deux provenances, parce que la plus utile est souvent la sauvegarde :
|
|
|
|
|
|
restaurer celle d'une instance Enterprise sur une installation
|
|
|
|
|
|
Community échoue — Odoo veut charger des modules qu'on n'a pas — alors
|
|
|
|
|
|
que les champs Studio ne sont que des lignes de `ir_model_fields`, et
|
|
|
|
|
|
qu'un dump.sql est du texte.
|
|
|
|
|
|
"""
|
|
|
|
|
|
from script.analyse import analyse_custom_field as analyse
|
|
|
|
|
|
|
|
|
|
|
|
target = self._analyse_select_source()
|
|
|
|
|
|
if not target:
|
|
|
|
|
|
return
|
|
|
|
|
|
is_backup, database = target
|
|
|
|
|
|
try:
|
|
|
|
|
|
data = (
|
|
|
|
|
|
analyse.collect_from_backup(database)
|
|
|
|
|
|
if is_backup
|
|
|
|
|
|
else analyse.collect(database)
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
print(f"❌ {t('Analysis failed: ')}{exc}")
|
|
|
|
|
|
return
|
|
|
|
|
|
print(analyse.render(data, hints=False))
|
|
|
|
|
|
if not data["fields"] and not data["models"]:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
def handler(rank):
|
|
|
|
|
|
if rank == 1:
|
|
|
|
|
|
print(analyse.render(data, verbose=True, hints=False))
|
|
|
|
|
|
else:
|
|
|
|
|
|
self._analyse_export_json(
|
|
|
|
|
|
data, os.path.basename(database), "custom_field"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
self._analyse_follow_up(
|
|
|
|
|
|
[
|
|
|
|
|
|
{"prompt_description": t("Show every field")},
|
|
|
|
|
|
{"prompt_description": t("Export as JSON")},
|
|
|
|
|
|
],
|
|
|
|
|
|
handler,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
[ADD] analyse: ausculter une base qui n'est pas ici
Les analyses existaient ; le chemin d'AVANT manquait. La base d'un client
est dans un zip, derrière une URL, ou vivante sur un serveur.
« Restant de migration » a dû être écrit : check_migration_quality compare
les bases de PALIER et exige le journal de progression — devant une
sauvegarde isolée, ni l'un ni l'autre n'existe.
Ses compteurs évidents ont été écartés après mesure. Comparés à la base
d'ORIGINE : champs sans colonne 25 → 72, modèles sans table 90 → 158.
Vingt-cinq et quatre-vingt-dix AVANT toute migration : du bruit. Ne
restent que les constats faux en eux-mêmes, 0 avant, non nuls après —
9 langues au drapeau NULL, 68 tables m2m absentes, 414 index doublés.
Le passe-plat RPC n'accepte que la lecture. psql l'obtient du serveur ;
une session RPC n'a rien d'équivalent, et la liste blanche est donc
appliquée dans le passe-plat, pas chez l'appelant.
--- EN ---
The analyses existed; the path BEFORE them did not. A customer database
sits in a zip, behind a URL, or live on a server.
« Migration leftovers » had to be written: check_migration_quality
compares STEP databases and needs the progression log — facing a lone
backup, neither exists.
Its obvious counters were dropped after measuring. Against the ORIGINAL
database: fields with no column 25 → 72, models with no table 90 → 158.
Twenty-five and ninety BEFORE any migration: noise. Only what is wrong in
itself remains, 0 before and non-zero after — 9 languages with a NULL
flag, 68 missing m2m tables, 414 duplicated indexes.
The RPC proxy only reads. psql gets that from the server; an RPC session
has no equivalent, so the allowlist lives in the proxy, not the caller.
Assisted-by: Claude Opus 5
2026-08-24 07:21:37 -04:00
|
|
|
|
def execute_analyse_monitoring(self):
|
|
|
|
|
|
"""Ausculter une instance dont la base n'est pas forcément ici.
|
|
|
|
|
|
|
|
|
|
|
|
Les analyses existent déjà ; ce qui manquait est le chemin d'avant.
|
|
|
|
|
|
Une sauvegarde, locale ou distante, se RESTAURE — après quoi tout
|
|
|
|
|
|
ce que le dépôt sait faire s'applique. Une instance vivante, non :
|
|
|
|
|
|
on n'a pas la base, on a une session, et ce que RPC laisse lire est
|
|
|
|
|
|
un sous-ensemble. L'écran de choix le dit au lieu de proposer des
|
|
|
|
|
|
analyses qui échoueraient à l'ouverture.
|
|
|
|
|
|
"""
|
|
|
|
|
|
from script.analyse import monitoring, monitoring_tui
|
|
|
|
|
|
|
|
|
|
|
|
print(f"🛰 {t('Inspect an instance.')}")
|
|
|
|
|
|
source = self._monitoring_select_source()
|
|
|
|
|
|
if not source:
|
|
|
|
|
|
return
|
|
|
|
|
|
kind, target = source
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(monitoring.describe_source(kind, target))
|
|
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
|
|
choix = monitoring_tui.run_tui(kind, target)
|
|
|
|
|
|
if not choix:
|
|
|
|
|
|
return
|
|
|
|
|
|
analyse = monitoring.analysis_by_key(choix)
|
|
|
|
|
|
if not analyse:
|
|
|
|
|
|
return
|
|
|
|
|
|
if kind not in analyse["kinds"]:
|
|
|
|
|
|
print(f"✖ {t('Not available for this source.')}")
|
|
|
|
|
|
return
|
[ADD] analyse: l'état d'une instance, lu pour l'usage qu'on en fait
Le même chiffre veut dire deux choses opposées. Zéro cron actif est le
succès attendu d'une copie et une panne totale sur une production. Un
rapport qui ignore cela crie au loup sur ce qu'on vient de demander, et
l'on cesse de le lire. L'attente est donc déclarée, copy ou live, et
chaque contrôle dit ce qu'il juge sous l'une et sous l'autre.
Deux contrôles ont été ÉCARTÉS sous copy après mesure : sur la base 12
d'origine, jamais démarrée, 11 crons étaient déjà en retard et db_backup
déjà vide — notre propre update_prod_to_dev les efface. Les afficher en
rouge aurait été du bruit ; en vert, un mensonge. Ils sont montrés non
jugés, avec la raison.
Le code Python en base a été mesuré et abandonné : 132 actions serveur,
zéro citant un modèle inexistant, et les 3 « modèles sans table » sont
ir.autovacuum et deux autres modèles abstraits d'Odoo.
--- EN ---
The same number means two opposite things. Zero active cron is the
expected success of a copy and a total outage on production. A report
that ignores this cries wolf over what was just requested, and stops
being read. The expectation is therefore declared, copy or live, and each
check states what it judges under either.
Two checks were DROPPED under copy after measuring: on the untouched 12
source database, 11 crons were already late and db_backup already empty —
our own update_prod_to_dev deletes them. Red would have been noise; green
a lie. They are shown unjudged, with the reason.
In-database Python was measured and dropped: 132 server actions, none
naming a missing model, and the 3 "models without a table" are
ir.autovacuum and two other Odoo abstract models.
Assisted-by: Claude Opus 5
2026-08-24 14:39:09 -04:00
|
|
|
|
extra = None
|
|
|
|
|
|
if analyse.get("asks_expect"):
|
|
|
|
|
|
extra = ["--expect", self._monitoring_expect(kind)]
|
[FIX] proxmox : l'ancienne entrée ssh s'en va avec la convention
Le nom chaîné devient systématique, mais les entrées écrites AVANT portent le
nom court — et rien ne les retirerait : elles ne déclarent pas le nom qu'on
écrit maintenant. Deux blocs mèneraient à la même machine, exactement ce
qu'on venait d'enlever.
Le ProxyJump tranche : un bloc qui rebondit par CET hôte est le nôtre, on le
retire. Celui d'une VM locale homonyme n'en a pas, et on n'y touche jamais ;
celui d'un autre hôte Proxmox non plus.
Le drapeau Odoo gagne son test au passage. Il tombait pour la même raison que
les colonnes vides — la sonde est le dernier maillon de la suite distante, et
un parc où une seule VM n'a pas d'Odoo, un hyperviseur imbriqué par exemple,
finit en échec. Vérifié sur les trois VM : l'hôte rend bien « ODOO » pour les
deux qui écoutent, et le navigateur répondait 303 pendant que la colonne
disait « — ».
--- EN ---
The chained name becomes systematic, but entries written BEFORE carry the
short one — and nothing would retire them: they do not declare the name we
now write. Two blocks would lead to the same machine, exactly what we had
just removed.
The ProxyJump decides: a block hopping through THIS host is ours, so it goes.
A local namesake's has none, and is never touched; another Proxmox host's
neither.
The Odoo flag gains its test along the way. It failed for the same reason as
the empty columns — the probe is the remote pipeline's last link, and a fleet
where a single VM has no Odoo, a nested hypervisor for instance, ends in
failure. Verified on all three VMs: the host does return "ODOO" for the two
that listen, and the browser answered 303 while the column said "—".
Assisted-by: Claude Opus 5
2026-08-25 00:43:10 -04:00
|
|
|
|
if analyse.get("writes"):
|
|
|
|
|
|
self._monitoring_write_flow(analyse, target)
|
|
|
|
|
|
return
|
[ADD] analyse: l'état d'une instance, lu pour l'usage qu'on en fait
Le même chiffre veut dire deux choses opposées. Zéro cron actif est le
succès attendu d'une copie et une panne totale sur une production. Un
rapport qui ignore cela crie au loup sur ce qu'on vient de demander, et
l'on cesse de le lire. L'attente est donc déclarée, copy ou live, et
chaque contrôle dit ce qu'il juge sous l'une et sous l'autre.
Deux contrôles ont été ÉCARTÉS sous copy après mesure : sur la base 12
d'origine, jamais démarrée, 11 crons étaient déjà en retard et db_backup
déjà vide — notre propre update_prod_to_dev les efface. Les afficher en
rouge aurait été du bruit ; en vert, un mensonge. Ils sont montrés non
jugés, avec la raison.
Le code Python en base a été mesuré et abandonné : 132 actions serveur,
zéro citant un modèle inexistant, et les 3 « modèles sans table » sont
ir.autovacuum et deux autres modèles abstraits d'Odoo.
--- EN ---
The same number means two opposite things. Zero active cron is the
expected success of a copy and a total outage on production. A report
that ignores this cries wolf over what was just requested, and stops
being read. The expectation is therefore declared, copy or live, and each
check states what it judges under either.
Two checks were DROPPED under copy after measuring: on the untouched 12
source database, 11 crons were already late and db_backup already empty —
our own update_prod_to_dev deletes them. Red would have been noise; green
a lie. They are shown unjudged, with the reason.
In-database Python was measured and dropped: 132 server actions, none
naming a missing model, and the 3 "models without a table" are
ir.autovacuum and two other Odoo abstract models.
Assisted-by: Claude Opus 5
2026-08-24 14:39:09 -04:00
|
|
|
|
monitoring.run_analysis(analyse, target, extra=extra)
|
|
|
|
|
|
|
[FIX] proxmox : l'ancienne entrée ssh s'en va avec la convention
Le nom chaîné devient systématique, mais les entrées écrites AVANT portent le
nom court — et rien ne les retirerait : elles ne déclarent pas le nom qu'on
écrit maintenant. Deux blocs mèneraient à la même machine, exactement ce
qu'on venait d'enlever.
Le ProxyJump tranche : un bloc qui rebondit par CET hôte est le nôtre, on le
retire. Celui d'une VM locale homonyme n'en a pas, et on n'y touche jamais ;
celui d'un autre hôte Proxmox non plus.
Le drapeau Odoo gagne son test au passage. Il tombait pour la même raison que
les colonnes vides — la sonde est le dernier maillon de la suite distante, et
un parc où une seule VM n'a pas d'Odoo, un hyperviseur imbriqué par exemple,
finit en échec. Vérifié sur les trois VM : l'hôte rend bien « ODOO » pour les
deux qui écoutent, et le navigateur répondait 303 pendant que la colonne
disait « — ».
--- EN ---
The chained name becomes systematic, but entries written BEFORE carry the
short one — and nothing would retire them: they do not declare the name we
now write. Two blocks would lead to the same machine, exactly what we had
just removed.
The ProxyJump decides: a block hopping through THIS host is ours, so it goes.
A local namesake's has none, and is never touched; another Proxmox host's
neither.
The Odoo flag gains its test along the way. It failed for the same reason as
the empty columns — the probe is the remote pipeline's last link, and a fleet
where a single VM has no Odoo, a nested hypervisor for instance, ends in
failure. Verified on all three VMs: the host does return "ODOO" for the two
that listen, and the browser answered 303 while the column said "—".
Assisted-by: Claude Opus 5
2026-08-25 00:43:10 -04:00
|
|
|
|
def _monitoring_write_flow(self, analyse, database):
|
|
|
|
|
|
"""La seule analyse qui écrit : montrer, puis demander.
|
|
|
|
|
|
|
|
|
|
|
|
On lance TOUJOURS la marche à blanc d'abord, et l'on demande
|
|
|
|
|
|
ensuite. Une question posée avant de savoir ce qui sera touché
|
|
|
|
|
|
n'est pas un consentement : c'est un pari. Le rapport dit combien
|
|
|
|
|
|
de modèles et de colonnes, et lesquels sont traduits ou uniques.
|
|
|
|
|
|
|
|
|
|
|
|
La confirmation redemande le NOM de la base. Une frappe sur « o »
|
[FIX] conventions : retirer un nom d'organisation des exemples
Un nom d'organisation servait d'exemple dans une docstring et dans quatre
tests — nom de base, chemin de compte, préfixe de copie. La règle du dépôt
l'interdit partout hors de `private/`, et ces fichiers suivent le dépôt en
amont : sur un fork rendu public, l'exemple le devient aussi.
Un test le fige pour toujours, et trois des cinq sites servaient justement à
démontrer qu'un chemin de compte ou un nom est refusé. Une valeur inventée
démontre aussi bien, et celle-ci ne paraît nulle part ailleurs. La docstring
perd son exemple : il n'illustrait que « long », et la phrase le dit
maintenant. Le détecteur du dépôt ne voit pas cette classe — il n'annonçait
rien ici.
--- EN ---
An organisation's name served as an example in one docstring and in four
tests — a database name, an account path, a copy prefix. The repository's rule
forbids it anywhere outside `private/`, and these files follow the repository
upstream: on a fork made public, the example becomes public too.
A test freezes it forever, and three of the five sites existed precisely to
demonstrate that an account path or a name is refused. An invented value
demonstrates just as well, and this one appears nowhere else. The docstring
loses its example: it only illustrated "long", which the sentence now says.
The repository's detector does not see this class — it reported nothing here.
Assisted-by: Claude Opus 5
2026-09-09 04:34:46 -04:00
|
|
|
|
se tape par réflexe ; recopier un nom long oblige à regarder ce
|
|
|
|
|
|
qu'on détruit.
|
[ADD] transform data : anonymiser un fichier externe ou une base Odoo
Partager un fichier ou une base demandait d'anonymiser à la main,
colonne par colonne, ou de ne pas partager. L'entrée ouvre un fichier
externe — Excel, Access, CSV, XML, JSON — ou une base Odoo, dit ce
qu'elle porte, puis en tire une copie. L'original n'est jamais touché.
Deux règles gouvernent ce qui sort. Une colonne n'est laissée intacte
que sur son CONTENU mesuré, hors onze étiquettes structurelles : décider
sur l'étiquette recopiait les colonnes les plus identifiantes en les
annonçant protégées. Et la copie est relue octet par octet, un classeur
portant de la donnée en une douzaine d'endroits hors des cellules.
Vérifié : 5 748 tests — 13 rouges, déjà rouges sur master.
--- EN ---
Sharing a file or a database meant anonymising by hand, column by
column, or not sharing. The entry opens an external file — Excel,
Access, CSV, XML, JSON — or an Odoo database, says what it holds, then
draws a copy of it. The original is never touched.
Two rules govern what comes out. A column is left alone on its measured
CONTENT, bar eleven structural labels: deciding on the label copied the
most identifying columns verbatim while announcing them as protected.
And the copy is reread byte by byte, a workbook holding data in a dozen
places that are not cells.
Checked: 5,748 tests — 13 red, already red on master.
Assisted-by: Claude Opus 5
2026-09-10 23:58:01 -04:00
|
|
|
|
|
|
|
|
|
|
Rend VRAI seulement si l'écriture a eu lieu. L'appelant qui tire
|
|
|
|
|
|
une sauvegarde derrière en a besoin : sans distinction, un
|
|
|
|
|
|
renoncement produisait un zip de la base NON anonymisée, annoncé
|
|
|
|
|
|
comme le résultat d'une anonymisation.
|
|
|
|
|
|
|
|
|
|
|
|
Les codes de sortie de la marche à blanc sont un CONTRAT, et le
|
|
|
|
|
|
travail s'annonce par un code À LUI — voir `anonymize.SORTIE_*`.
|
|
|
|
|
|
Lire « tout ce qui n'est ni 0 ni 2 » comme du travail faisait
|
|
|
|
|
|
demander la confirmation destructrice après une trace Python,
|
|
|
|
|
|
laquelle sort en 1, puis « appliquer » un plan jamais calculé.
|
[FIX] proxmox : l'ancienne entrée ssh s'en va avec la convention
Le nom chaîné devient systématique, mais les entrées écrites AVANT portent le
nom court — et rien ne les retirerait : elles ne déclarent pas le nom qu'on
écrit maintenant. Deux blocs mèneraient à la même machine, exactement ce
qu'on venait d'enlever.
Le ProxyJump tranche : un bloc qui rebondit par CET hôte est le nôtre, on le
retire. Celui d'une VM locale homonyme n'en a pas, et on n'y touche jamais ;
celui d'un autre hôte Proxmox non plus.
Le drapeau Odoo gagne son test au passage. Il tombait pour la même raison que
les colonnes vides — la sonde est le dernier maillon de la suite distante, et
un parc où une seule VM n'a pas d'Odoo, un hyperviseur imbriqué par exemple,
finit en échec. Vérifié sur les trois VM : l'hôte rend bien « ODOO » pour les
deux qui écoutent, et le navigateur répondait 303 pendant que la colonne
disait « — ».
--- EN ---
The chained name becomes systematic, but entries written BEFORE carry the
short one — and nothing would retire them: they do not declare the name we
now write. Two blocks would lead to the same machine, exactly what we had
just removed.
The ProxyJump decides: a block hopping through THIS host is ours, so it goes.
A local namesake's has none, and is never touched; another Proxmox host's
neither.
The Odoo flag gains its test along the way. It failed for the same reason as
the empty columns — the probe is the remote pipeline's last link, and a fleet
where a single VM has no Odoo, a nested hypervisor for instance, ends in
failure. Verified on all three VMs: the host does return "ODOO" for the two
that listen, and the browser answered 303 while the column said "—".
Assisted-by: Claude Opus 5
2026-08-25 00:43:10 -04:00
|
|
|
|
"""
|
[ADD] transform data : anonymiser un fichier externe ou une base Odoo
Partager un fichier ou une base demandait d'anonymiser à la main,
colonne par colonne, ou de ne pas partager. L'entrée ouvre un fichier
externe — Excel, Access, CSV, XML, JSON — ou une base Odoo, dit ce
qu'elle porte, puis en tire une copie. L'original n'est jamais touché.
Deux règles gouvernent ce qui sort. Une colonne n'est laissée intacte
que sur son CONTENU mesuré, hors onze étiquettes structurelles : décider
sur l'étiquette recopiait les colonnes les plus identifiantes en les
annonçant protégées. Et la copie est relue octet par octet, un classeur
portant de la donnée en une douzaine d'endroits hors des cellules.
Vérifié : 5 748 tests — 13 rouges, déjà rouges sur master.
--- EN ---
Sharing a file or a database meant anonymising by hand, column by
column, or not sharing. The entry opens an external file — Excel,
Access, CSV, XML, JSON — or an Odoo database, says what it holds, then
draws a copy of it. The original is never touched.
Two rules govern what comes out. A column is left alone on its measured
CONTENT, bar eleven structural labels: deciding on the label copied the
most identifying columns verbatim while announcing them as protected.
And the copy is reread byte by byte, a workbook holding data in a dozen
places that are not cells.
Checked: 5,748 tests — 13 red, already red on master.
Assisted-by: Claude Opus 5
2026-09-10 23:58:01 -04:00
|
|
|
|
from script.analyse import anonymize, monitoring
|
[FIX] proxmox : l'ancienne entrée ssh s'en va avec la convention
Le nom chaîné devient systématique, mais les entrées écrites AVANT portent le
nom court — et rien ne les retirerait : elles ne déclarent pas le nom qu'on
écrit maintenant. Deux blocs mèneraient à la même machine, exactement ce
qu'on venait d'enlever.
Le ProxyJump tranche : un bloc qui rebondit par CET hôte est le nôtre, on le
retire. Celui d'une VM locale homonyme n'en a pas, et on n'y touche jamais ;
celui d'un autre hôte Proxmox non plus.
Le drapeau Odoo gagne son test au passage. Il tombait pour la même raison que
les colonnes vides — la sonde est le dernier maillon de la suite distante, et
un parc où une seule VM n'a pas d'Odoo, un hyperviseur imbriqué par exemple,
finit en échec. Vérifié sur les trois VM : l'hôte rend bien « ODOO » pour les
deux qui écoutent, et le navigateur répondait 303 pendant que la colonne
disait « — ».
--- EN ---
The chained name becomes systematic, but entries written BEFORE carry the
short one — and nothing would retire them: they do not declare the name we
now write. Two blocks would lead to the same machine, exactly what we had
just removed.
The ProxyJump decides: a block hopping through THIS host is ours, so it goes.
A local namesake's has none, and is never touched; another Proxmox host's
neither.
The Odoo flag gains its test along the way. It failed for the same reason as
the empty columns — the probe is the remote pipeline's last link, and a fleet
where a single VM has no Odoo, a nested hypervisor for instance, ends in
failure. Verified on all three VMs: the host does return "ODOO" for the two
that listen, and the browser answered 303 while the column said "—".
Assisted-by: Claude Opus 5
2026-08-25 00:43:10 -04:00
|
|
|
|
|
|
|
|
|
|
choix = self._monitoring_anonymize_options()
|
|
|
|
|
|
if choix is None:
|
[ADD] transform data : anonymiser un fichier externe ou une base Odoo
Partager un fichier ou une base demandait d'anonymiser à la main,
colonne par colonne, ou de ne pas partager. L'entrée ouvre un fichier
externe — Excel, Access, CSV, XML, JSON — ou une base Odoo, dit ce
qu'elle porte, puis en tire une copie. L'original n'est jamais touché.
Deux règles gouvernent ce qui sort. Une colonne n'est laissée intacte
que sur son CONTENU mesuré, hors onze étiquettes structurelles : décider
sur l'étiquette recopiait les colonnes les plus identifiantes en les
annonçant protégées. Et la copie est relue octet par octet, un classeur
portant de la donnée en une douzaine d'endroits hors des cellules.
Vérifié : 5 748 tests — 13 rouges, déjà rouges sur master.
--- EN ---
Sharing a file or a database meant anonymising by hand, column by
column, or not sharing. The entry opens an external file — Excel,
Access, CSV, XML, JSON — or an Odoo database, says what it holds, then
draws a copy of it. The original is never touched.
Two rules govern what comes out. A column is left alone on its measured
CONTENT, bar eleven structural labels: deciding on the label copied the
most identifying columns verbatim while announcing them as protected.
And the copy is reread byte by byte, a workbook holding data in a dozen
places that are not cells.
Checked: 5,748 tests — 13 red, already red on master.
Assisted-by: Claude Opus 5
2026-09-10 23:58:01 -04:00
|
|
|
|
return False
|
[FIX] proxmox : l'ancienne entrée ssh s'en va avec la convention
Le nom chaîné devient systématique, mais les entrées écrites AVANT portent le
nom court — et rien ne les retirerait : elles ne déclarent pas le nom qu'on
écrit maintenant. Deux blocs mèneraient à la même machine, exactement ce
qu'on venait d'enlever.
Le ProxyJump tranche : un bloc qui rebondit par CET hôte est le nôtre, on le
retire. Celui d'une VM locale homonyme n'en a pas, et on n'y touche jamais ;
celui d'un autre hôte Proxmox non plus.
Le drapeau Odoo gagne son test au passage. Il tombait pour la même raison que
les colonnes vides — la sonde est le dernier maillon de la suite distante, et
un parc où une seule VM n'a pas d'Odoo, un hyperviseur imbriqué par exemple,
finit en échec. Vérifié sur les trois VM : l'hôte rend bien « ODOO » pour les
deux qui écoutent, et le navigateur répondait 303 pendant que la colonne
disait « — ».
--- EN ---
The chained name becomes systematic, but entries written BEFORE carry the
short one — and nothing would retire them: they do not declare the name we
now write. Two blocks would lead to the same machine, exactly what we had
just removed.
The ProxyJump decides: a block hopping through THIS host is ours, so it goes.
A local namesake's has none, and is never touched; another Proxmox host's
neither.
The Odoo flag gains its test along the way. It failed for the same reason as
the empty columns — the probe is the remote pipeline's last link, and a fleet
where a single VM has no Odoo, a nested hypervisor for instance, ends in
failure. Verified on all three VMs: the host does return "ODOO" for the two
that listen, and the browser answered 303 while the column said "—".
Assisted-by: Claude Opus 5
2026-08-25 00:43:10 -04:00
|
|
|
|
print()
|
[ADD] transform data : anonymiser un fichier externe ou une base Odoo
Partager un fichier ou une base demandait d'anonymiser à la main,
colonne par colonne, ou de ne pas partager. L'entrée ouvre un fichier
externe — Excel, Access, CSV, XML, JSON — ou une base Odoo, dit ce
qu'elle porte, puis en tire une copie. L'original n'est jamais touché.
Deux règles gouvernent ce qui sort. Une colonne n'est laissée intacte
que sur son CONTENU mesuré, hors onze étiquettes structurelles : décider
sur l'étiquette recopiait les colonnes les plus identifiantes en les
annonçant protégées. Et la copie est relue octet par octet, un classeur
portant de la donnée en une douzaine d'endroits hors des cellules.
Vérifié : 5 748 tests — 13 rouges, déjà rouges sur master.
--- EN ---
Sharing a file or a database meant anonymising by hand, column by
column, or not sharing. The entry opens an external file — Excel,
Access, CSV, XML, JSON — or an Odoo database, says what it holds, then
draws a copy of it. The original is never touched.
Two rules govern what comes out. A column is left alone on its measured
CONTENT, bar eleven structural labels: deciding on the label copied the
most identifying columns verbatim while announcing them as protected.
And the copy is reread byte by byte, a workbook holding data in a dozen
places that are not cells.
Checked: 5,748 tests — 13 red, already red on master.
Assisted-by: Claude Opus 5
2026-09-10 23:58:01 -04:00
|
|
|
|
code = monitoring.run_analysis(analyse, database, extra=choix)
|
|
|
|
|
|
if code != anonymize.SORTIE_A_FAIRE:
|
|
|
|
|
|
if code == anonymize.SORTIE_RIEN:
|
|
|
|
|
|
# Demander de retaper le nom d'une base qu'on ne touchera
|
|
|
|
|
|
# pas obtient un consentement sans objet, et l'appelant
|
|
|
|
|
|
# tirait ensuite une sauvegarde de la base INTACTE en
|
|
|
|
|
|
# l'annonçant anonymisée.
|
|
|
|
|
|
print(f"↩️ {t('Nothing to anonymise: nothing to confirm.')}")
|
|
|
|
|
|
elif code != anonymize.SORTIE_REFUS:
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"❌ {t('The dry run ended on an unexpected code:')}"
|
|
|
|
|
|
f" {code}"
|
|
|
|
|
|
)
|
|
|
|
|
|
return False
|
[FIX] proxmox : l'ancienne entrée ssh s'en va avec la convention
Le nom chaîné devient systématique, mais les entrées écrites AVANT portent le
nom court — et rien ne les retirerait : elles ne déclarent pas le nom qu'on
écrit maintenant. Deux blocs mèneraient à la même machine, exactement ce
qu'on venait d'enlever.
Le ProxyJump tranche : un bloc qui rebondit par CET hôte est le nôtre, on le
retire. Celui d'une VM locale homonyme n'en a pas, et on n'y touche jamais ;
celui d'un autre hôte Proxmox non plus.
Le drapeau Odoo gagne son test au passage. Il tombait pour la même raison que
les colonnes vides — la sonde est le dernier maillon de la suite distante, et
un parc où une seule VM n'a pas d'Odoo, un hyperviseur imbriqué par exemple,
finit en échec. Vérifié sur les trois VM : l'hôte rend bien « ODOO » pour les
deux qui écoutent, et le navigateur répondait 303 pendant que la colonne
disait « — ».
--- EN ---
The chained name becomes systematic, but entries written BEFORE carry the
short one — and nothing would retire them: they do not declare the name we
now write. Two blocks would lead to the same machine, exactly what we had
just removed.
The ProxyJump decides: a block hopping through THIS host is ours, so it goes.
A local namesake's has none, and is never touched; another Proxmox host's
neither.
The Odoo flag gains its test along the way. It failed for the same reason as
the empty columns — the probe is the remote pipeline's last link, and a fleet
where a single VM has no Odoo, a nested hypervisor for instance, ends in
failure. Verified on all three VMs: the host does return "ODOO" for the two
that listen, and the browser answered 303 while the column said "—".
Assisted-by: Claude Opus 5
2026-08-25 00:43:10 -04:00
|
|
|
|
print()
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"⚠️ {t('This DESTROYS the data of')} '{database}'"
|
|
|
|
|
|
f" — {t('there is no undo.')}"
|
|
|
|
|
|
)
|
|
|
|
|
|
tape = input(
|
|
|
|
|
|
f"💬 {t('Type the database name to confirm (empty to cancel): ')}"
|
|
|
|
|
|
).strip()
|
|
|
|
|
|
if tape != database:
|
|
|
|
|
|
print(f"↩️ {t('Cancelled: nothing was written.')}")
|
[ADD] transform data : anonymiser un fichier externe ou une base Odoo
Partager un fichier ou une base demandait d'anonymiser à la main,
colonne par colonne, ou de ne pas partager. L'entrée ouvre un fichier
externe — Excel, Access, CSV, XML, JSON — ou une base Odoo, dit ce
qu'elle porte, puis en tire une copie. L'original n'est jamais touché.
Deux règles gouvernent ce qui sort. Une colonne n'est laissée intacte
que sur son CONTENU mesuré, hors onze étiquettes structurelles : décider
sur l'étiquette recopiait les colonnes les plus identifiantes en les
annonçant protégées. Et la copie est relue octet par octet, un classeur
portant de la donnée en une douzaine d'endroits hors des cellules.
Vérifié : 5 748 tests — 13 rouges, déjà rouges sur master.
--- EN ---
Sharing a file or a database meant anonymising by hand, column by
column, or not sharing. The entry opens an external file — Excel,
Access, CSV, XML, JSON — or an Odoo database, says what it holds, then
draws a copy of it. The original is never touched.
Two rules govern what comes out. A column is left alone on its measured
CONTENT, bar eleven structural labels: deciding on the label copied the
most identifying columns verbatim while announcing them as protected.
And the copy is reread byte by byte, a workbook holding data in a dozen
places that are not cells.
Checked: 5,748 tests — 13 red, already red on master.
Assisted-by: Claude Opus 5
2026-09-10 23:58:01 -04:00
|
|
|
|
return False
|
|
|
|
|
|
# SEUL le code de succès vaut « l'écriture a eu lieu » : un plan
|
|
|
|
|
|
# devenu vide entre les deux passes rend SORTIE_SANS_EFFET, et non
|
|
|
|
|
|
# le 0 que psql donnait sur un script vide.
|
|
|
|
|
|
return (
|
|
|
|
|
|
monitoring.run_analysis(
|
|
|
|
|
|
analyse,
|
|
|
|
|
|
database,
|
|
|
|
|
|
extra=choix + ["--apply", "--confirm", database],
|
|
|
|
|
|
)
|
|
|
|
|
|
== anonymize.SORTIE_RIEN
|
[FIX] proxmox : l'ancienne entrée ssh s'en va avec la convention
Le nom chaîné devient systématique, mais les entrées écrites AVANT portent le
nom court — et rien ne les retirerait : elles ne déclarent pas le nom qu'on
écrit maintenant. Deux blocs mèneraient à la même machine, exactement ce
qu'on venait d'enlever.
Le ProxyJump tranche : un bloc qui rebondit par CET hôte est le nôtre, on le
retire. Celui d'une VM locale homonyme n'en a pas, et on n'y touche jamais ;
celui d'un autre hôte Proxmox non plus.
Le drapeau Odoo gagne son test au passage. Il tombait pour la même raison que
les colonnes vides — la sonde est le dernier maillon de la suite distante, et
un parc où une seule VM n'a pas d'Odoo, un hyperviseur imbriqué par exemple,
finit en échec. Vérifié sur les trois VM : l'hôte rend bien « ODOO » pour les
deux qui écoutent, et le navigateur répondait 303 pendant que la colonne
disait « — ».
--- EN ---
The chained name becomes systematic, but entries written BEFORE carry the
short one — and nothing would retire them: they do not declare the name we
now write. Two blocks would lead to the same machine, exactly what we had
just removed.
The ProxyJump decides: a block hopping through THIS host is ours, so it goes.
A local namesake's has none, and is never touched; another Proxmox host's
neither.
The Odoo flag gains its test along the way. It failed for the same reason as
the empty columns — the probe is the remote pipeline's last link, and a fleet
where a single VM has no Odoo, a nested hypervisor for instance, ends in
failure. Verified on all three VMs: the host does return "ODOO" for the two
that listen, and the browser answered 303 while the column said "—".
Assisted-by: Claude Opus 5
2026-08-25 00:43:10 -04:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _monitoring_anonymize_options(self):
|
|
|
|
|
|
"""Le mode et ses listes, ou None si l'on renonce."""
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(f"[1] {t('Hybrid: the default personal-data models, adjusted')}")
|
|
|
|
|
|
print(f"[2] {t('Whitelist: only the models I name')}")
|
|
|
|
|
|
print(f"[3] {t('Blacklist: every model except those I name')}")
|
|
|
|
|
|
print(f"[0] {t('Back')}")
|
[FIX] todo : une invite n'écrit plus son deux-points deux fois
`click.prompt` ajoute son propre « : » à l'étiquette qu'on lui donne, et
quinze étiquettes le portaient déjà. Le menu le plus vu du logiciel demandait
donc « Commande :: », et sept invites de déploiement à distance affichaient
un deux-points suivi d'un autre.
Le suffixe est désormais explicite partout où l'étiquette ponctue elle-même,
et sa valeur se déduit de l'étiquette : rien quand elle porte déjà l'espace
qui suit, un espace quand le deux-points est nu. Un contrôle lit l'arbre et
résout les DEUX langues, le français mettant une espace avant le deux-points
là où l'anglais n'en met pas — chercher dans le texte du code ne verrait que
la clé anglaise.
--- EN ---
`click.prompt` adds its own ":" to the label it is given, and fifteen labels
already carried one. The most-seen menu of the software therefore asked
"Command:: ", and seven remote-deployment prompts showed a colon followed by
another.
The suffix is now explicit wherever the label punctuates itself, and its value
follows from the label: nothing when it already carries the trailing space, a
space when the colon is bare. A check reads the tree and resolves BOTH
languages, French putting a space before the colon where English does not —
searching the code text would only see the English key.
Assisted-by: Claude Opus 5
2026-09-09 05:32:52 -04:00
|
|
|
|
answer = click.prompt(t("Command:"), prompt_suffix=" ")
|
[FIX] proxmox : l'ancienne entrée ssh s'en va avec la convention
Le nom chaîné devient systématique, mais les entrées écrites AVANT portent le
nom court — et rien ne les retirerait : elles ne déclarent pas le nom qu'on
écrit maintenant. Deux blocs mèneraient à la même machine, exactement ce
qu'on venait d'enlever.
Le ProxyJump tranche : un bloc qui rebondit par CET hôte est le nôtre, on le
retire. Celui d'une VM locale homonyme n'en a pas, et on n'y touche jamais ;
celui d'un autre hôte Proxmox non plus.
Le drapeau Odoo gagne son test au passage. Il tombait pour la même raison que
les colonnes vides — la sonde est le dernier maillon de la suite distante, et
un parc où une seule VM n'a pas d'Odoo, un hyperviseur imbriqué par exemple,
finit en échec. Vérifié sur les trois VM : l'hôte rend bien « ODOO » pour les
deux qui écoutent, et le navigateur répondait 303 pendant que la colonne
disait « — ».
--- EN ---
The chained name becomes systematic, but entries written BEFORE carry the
short one — and nothing would retire them: they do not declare the name we
now write. Two blocks would lead to the same machine, exactly what we had
just removed.
The ProxyJump decides: a block hopping through THIS host is ours, so it goes.
A local namesake's has none, and is never touched; another Proxmox host's
neither.
The Odoo flag gains its test along the way. It failed for the same reason as
the empty columns — the probe is the remote pipeline's last link, and a fleet
where a single VM has no Odoo, a nested hypervisor for instance, ends in
failure. Verified on all three VMs: the host does return "ODOO" for the two
that listen, and the browser answered 303 while the column said "—".
Assisted-by: Claude Opus 5
2026-08-25 00:43:10 -04:00
|
|
|
|
print()
|
|
|
|
|
|
mode = {"1": "hybrid", "2": "whitelist", "3": "blacklist"}.get(answer)
|
|
|
|
|
|
if not mode:
|
|
|
|
|
|
return None
|
|
|
|
|
|
extra = ["--mode", mode]
|
|
|
|
|
|
invite = (
|
|
|
|
|
|
t("Models to ADD, comma separated (empty for none): ")
|
|
|
|
|
|
if mode != "blacklist"
|
|
|
|
|
|
else t("Models to EXCLUDE, comma separated: ")
|
|
|
|
|
|
)
|
|
|
|
|
|
noms = input(f"💬 {invite}").strip()
|
|
|
|
|
|
if noms:
|
|
|
|
|
|
extra += ["--exclude" if mode == "blacklist" else "--models", noms]
|
|
|
|
|
|
elif mode == "whitelist":
|
|
|
|
|
|
print(f"❌ {t('A whitelist with no model would do nothing.')}")
|
|
|
|
|
|
return None
|
[ADD] transform data : anonymiser un fichier externe ou une base Odoo
Partager un fichier ou une base demandait d'anonymiser à la main,
colonne par colonne, ou de ne pas partager. L'entrée ouvre un fichier
externe — Excel, Access, CSV, XML, JSON — ou une base Odoo, dit ce
qu'elle porte, puis en tire une copie. L'original n'est jamais touché.
Deux règles gouvernent ce qui sort. Une colonne n'est laissée intacte
que sur son CONTENU mesuré, hors onze étiquettes structurelles : décider
sur l'étiquette recopiait les colonnes les plus identifiantes en les
annonçant protégées. Et la copie est relue octet par octet, un classeur
portant de la donnée en une douzaine d'endroits hors des cellules.
Vérifié : 5 748 tests — 13 rouges, déjà rouges sur master.
--- EN ---
Sharing a file or a database meant anonymising by hand, column by
column, or not sharing. The entry opens an external file — Excel,
Access, CSV, XML, JSON — or an Odoo database, says what it holds, then
draws a copy of it. The original is never touched.
Two rules govern what comes out. A column is left alone on its measured
CONTENT, bar eleven structural labels: deciding on the label copied the
most identifying columns verbatim while announcing them as protected.
And the copy is reread byte by byte, a workbook holding data in a dozen
places that are not cells.
Checked: 5,748 tests — 13 red, already red on master.
Assisted-by: Claude Opus 5
2026-09-10 23:58:01 -04:00
|
|
|
|
# Les logins RESTENT par défaut : on anonymise pour pouvoir
|
|
|
|
|
|
# partager une copie UTILISABLE, et tout randomiser empêcherait
|
|
|
|
|
|
# quiconque de s'y connecter.
|
|
|
|
|
|
if self._is_yes(input(f"💬 {t('Anonymise the logins too? (y/N): ')}")):
|
|
|
|
|
|
extra.append("--include-logins")
|
|
|
|
|
|
# Le calibre échange une garantie contre une autre : l'étendue
|
|
|
|
|
|
# mesurée protège les bornes que le CODE d'Odoo impose — une heure
|
|
|
|
|
|
# de la journée, une probabilité — et la largeur sert un export
|
|
|
|
|
|
# relu à l'œil. L'invite le dit avant de demander.
|
|
|
|
|
|
print(f" {t('Keeping the digit count drops the measured extent.')}")
|
|
|
|
|
|
if self._is_yes(input(f"💬 {t('Keep the digit count? (y/N): ')}")):
|
|
|
|
|
|
extra.append("--keep-digits")
|
[FIX] proxmox : l'ancienne entrée ssh s'en va avec la convention
Le nom chaîné devient systématique, mais les entrées écrites AVANT portent le
nom court — et rien ne les retirerait : elles ne déclarent pas le nom qu'on
écrit maintenant. Deux blocs mèneraient à la même machine, exactement ce
qu'on venait d'enlever.
Le ProxyJump tranche : un bloc qui rebondit par CET hôte est le nôtre, on le
retire. Celui d'une VM locale homonyme n'en a pas, et on n'y touche jamais ;
celui d'un autre hôte Proxmox non plus.
Le drapeau Odoo gagne son test au passage. Il tombait pour la même raison que
les colonnes vides — la sonde est le dernier maillon de la suite distante, et
un parc où une seule VM n'a pas d'Odoo, un hyperviseur imbriqué par exemple,
finit en échec. Vérifié sur les trois VM : l'hôte rend bien « ODOO » pour les
deux qui écoutent, et le navigateur répondait 303 pendant que la colonne
disait « — ».
--- EN ---
The chained name becomes systematic, but entries written BEFORE carry the
short one — and nothing would retire them: they do not declare the name we
now write. Two blocks would lead to the same machine, exactly what we had
just removed.
The ProxyJump decides: a block hopping through THIS host is ours, so it goes.
A local namesake's has none, and is never touched; another Proxmox host's
neither.
The Odoo flag gains its test along the way. It failed for the same reason as
the empty columns — the probe is the remote pipeline's last link, and a fleet
where a single VM has no Odoo, a nested hypervisor for instance, ends in
failure. Verified on all three VMs: the host does return "ODOO" for the two
that listen, and the browser answered 303 while the column said "—".
Assisted-by: Claude Opus 5
2026-08-25 00:43:10 -04:00
|
|
|
|
mots = input(
|
|
|
|
|
|
f"💬 {t('Python file declaring MOTS (empty for the built-in): ')}"
|
|
|
|
|
|
).strip()
|
|
|
|
|
|
if mots:
|
|
|
|
|
|
if not os.path.isfile(os.path.expanduser(mots)):
|
|
|
|
|
|
print(f"❌ {t('No such file: ')}{mots}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
extra += ["--words", os.path.expanduser(mots)]
|
|
|
|
|
|
return extra
|
|
|
|
|
|
|
[ADD] analyse: l'état d'une instance, lu pour l'usage qu'on en fait
Le même chiffre veut dire deux choses opposées. Zéro cron actif est le
succès attendu d'une copie et une panne totale sur une production. Un
rapport qui ignore cela crie au loup sur ce qu'on vient de demander, et
l'on cesse de le lire. L'attente est donc déclarée, copy ou live, et
chaque contrôle dit ce qu'il juge sous l'une et sous l'autre.
Deux contrôles ont été ÉCARTÉS sous copy après mesure : sur la base 12
d'origine, jamais démarrée, 11 crons étaient déjà en retard et db_backup
déjà vide — notre propre update_prod_to_dev les efface. Les afficher en
rouge aurait été du bruit ; en vert, un mensonge. Ils sont montrés non
jugés, avec la raison.
Le code Python en base a été mesuré et abandonné : 132 actions serveur,
zéro citant un modèle inexistant, et les 3 « modèles sans table » sont
ir.autovacuum et deux autres modèles abstraits d'Odoo.
--- EN ---
The same number means two opposite things. Zero active cron is the
expected success of a copy and a total outage on production. A report
that ignores this cries wolf over what was just requested, and stops
being read. The expectation is therefore declared, copy or live, and each
check states what it judges under either.
Two checks were DROPPED under copy after measuring: on the untouched 12
source database, 11 crons were already late and db_backup already empty —
our own update_prod_to_dev deletes them. Red would have been noise; green
a lie. They are shown unjudged, with the reason.
In-database Python was measured and dropped: 132 server actions, none
naming a missing model, and the 3 "models without a table" are
ir.autovacuum and two other Odoo abstract models.
Assisted-by: Claude Opus 5
2026-08-24 14:39:09 -04:00
|
|
|
|
def _monitoring_expect(self, kind):
|
|
|
|
|
|
"""Copie de développement, ou instance en service ?
|
|
|
|
|
|
|
|
|
|
|
|
Zéro cron actif est le SUCCÈS attendu d'une copie et une panne
|
|
|
|
|
|
totale sur une production ; zéro serveur de courriel rassure sur
|
|
|
|
|
|
l'une et condamne l'autre. Deviner à la place de l'utilisateur,
|
|
|
|
|
|
c'est afficher du rouge sur ce qu'il vient de demander — et l'on
|
|
|
|
|
|
cesse alors de lire le rapport.
|
|
|
|
|
|
"""
|
|
|
|
|
|
from script.analyse import check_instance_state, monitoring
|
|
|
|
|
|
|
|
|
|
|
|
if kind == monitoring.KIND_LIVE:
|
|
|
|
|
|
return check_instance_state.LIVE
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(f"[1] {t('A development copy (restored, neutralised)')}")
|
|
|
|
|
|
print(f"[2] {t('An instance in service')}")
|
[FIX] todo : une invite n'écrit plus son deux-points deux fois
`click.prompt` ajoute son propre « : » à l'étiquette qu'on lui donne, et
quinze étiquettes le portaient déjà. Le menu le plus vu du logiciel demandait
donc « Commande :: », et sept invites de déploiement à distance affichaient
un deux-points suivi d'un autre.
Le suffixe est désormais explicite partout où l'étiquette ponctue elle-même,
et sa valeur se déduit de l'étiquette : rien quand elle porte déjà l'espace
qui suit, un espace quand le deux-points est nu. Un contrôle lit l'arbre et
résout les DEUX langues, le français mettant une espace avant le deux-points
là où l'anglais n'en met pas — chercher dans le texte du code ne verrait que
la clé anglaise.
--- EN ---
`click.prompt` adds its own ":" to the label it is given, and fifteen labels
already carried one. The most-seen menu of the software therefore asked
"Command:: ", and seven remote-deployment prompts showed a colon followed by
another.
The suffix is now explicit wherever the label punctuates itself, and its value
follows from the label: nothing when it already carries the trailing space, a
space when the colon is bare. A check reads the tree and resolves BOTH
languages, French putting a space before the colon where English does not —
searching the code text would only see the English key.
Assisted-by: Claude Opus 5
2026-09-09 05:32:52 -04:00
|
|
|
|
answer = click.prompt(t("Command:"), prompt_suffix=" ")
|
[ADD] analyse: l'état d'une instance, lu pour l'usage qu'on en fait
Le même chiffre veut dire deux choses opposées. Zéro cron actif est le
succès attendu d'une copie et une panne totale sur une production. Un
rapport qui ignore cela crie au loup sur ce qu'on vient de demander, et
l'on cesse de le lire. L'attente est donc déclarée, copy ou live, et
chaque contrôle dit ce qu'il juge sous l'une et sous l'autre.
Deux contrôles ont été ÉCARTÉS sous copy après mesure : sur la base 12
d'origine, jamais démarrée, 11 crons étaient déjà en retard et db_backup
déjà vide — notre propre update_prod_to_dev les efface. Les afficher en
rouge aurait été du bruit ; en vert, un mensonge. Ils sont montrés non
jugés, avec la raison.
Le code Python en base a été mesuré et abandonné : 132 actions serveur,
zéro citant un modèle inexistant, et les 3 « modèles sans table » sont
ir.autovacuum et deux autres modèles abstraits d'Odoo.
--- EN ---
The same number means two opposite things. Zero active cron is the
expected success of a copy and a total outage on production. A report
that ignores this cries wolf over what was just requested, and stops
being read. The expectation is therefore declared, copy or live, and each
check states what it judges under either.
Two checks were DROPPED under copy after measuring: on the untouched 12
source database, 11 crons were already late and db_backup already empty —
our own update_prod_to_dev deletes them. Red would have been noise; green
a lie. They are shown unjudged, with the reason.
In-database Python was measured and dropped: 132 server actions, none
naming a missing model, and the 3 "models without a table" are
ir.autovacuum and two other Odoo abstract models.
Assisted-by: Claude Opus 5
2026-08-24 14:39:09 -04:00
|
|
|
|
print()
|
|
|
|
|
|
return (
|
|
|
|
|
|
check_instance_state.LIVE
|
|
|
|
|
|
if answer == "2"
|
|
|
|
|
|
else check_instance_state.COPY
|
|
|
|
|
|
)
|
[ADD] analyse: ausculter une base qui n'est pas ici
Les analyses existaient ; le chemin d'AVANT manquait. La base d'un client
est dans un zip, derrière une URL, ou vivante sur un serveur.
« Restant de migration » a dû être écrit : check_migration_quality compare
les bases de PALIER et exige le journal de progression — devant une
sauvegarde isolée, ni l'un ni l'autre n'existe.
Ses compteurs évidents ont été écartés après mesure. Comparés à la base
d'ORIGINE : champs sans colonne 25 → 72, modèles sans table 90 → 158.
Vingt-cinq et quatre-vingt-dix AVANT toute migration : du bruit. Ne
restent que les constats faux en eux-mêmes, 0 avant, non nuls après —
9 langues au drapeau NULL, 68 tables m2m absentes, 414 index doublés.
Le passe-plat RPC n'accepte que la lecture. psql l'obtient du serveur ;
une session RPC n'a rien d'équivalent, et la liste blanche est donc
appliquée dans le passe-plat, pas chez l'appelant.
--- EN ---
The analyses existed; the path BEFORE them did not. A customer database
sits in a zip, behind a URL, or live on a server.
« Migration leftovers » had to be written: check_migration_quality
compares STEP databases and needs the progression log — facing a lone
backup, neither exists.
Its obvious counters were dropped after measuring. Against the ORIGINAL
database: fields with no column 25 → 72, models with no table 90 → 158.
Twenty-five and ninety BEFORE any migration: noise. Only what is wrong in
itself remains, 0 before and non-zero after — 9 languages with a NULL
flag, 68 missing m2m tables, 414 duplicated indexes.
The RPC proxy only reads. psql gets that from the server; an RPC session
has no equivalent, so the allowlist lives in the proxy, not the caller.
Assisted-by: Claude Opus 5
2026-08-24 07:21:37 -04:00
|
|
|
|
|
|
|
|
|
|
def _monitoring_select_source(self):
|
|
|
|
|
|
"""(genre, cible), ou None si l'on renonce.
|
|
|
|
|
|
|
|
|
|
|
|
Le zip et la sauvegarde distante mènent tous deux à une base
|
|
|
|
|
|
restaurée : passé la restauration, ils ne se distinguent plus, et
|
|
|
|
|
|
le reste du code n'a pas à savoir d'où ils venaient.
|
|
|
|
|
|
"""
|
|
|
|
|
|
from script.analyse import monitoring
|
|
|
|
|
|
|
|
|
|
|
|
print()
|
[ADD] analyse: l'état d'une instance, lu pour l'usage qu'on en fait
Le même chiffre veut dire deux choses opposées. Zéro cron actif est le
succès attendu d'une copie et une panne totale sur une production. Un
rapport qui ignore cela crie au loup sur ce qu'on vient de demander, et
l'on cesse de le lire. L'attente est donc déclarée, copy ou live, et
chaque contrôle dit ce qu'il juge sous l'une et sous l'autre.
Deux contrôles ont été ÉCARTÉS sous copy après mesure : sur la base 12
d'origine, jamais démarrée, 11 crons étaient déjà en retard et db_backup
déjà vide — notre propre update_prod_to_dev les efface. Les afficher en
rouge aurait été du bruit ; en vert, un mensonge. Ils sont montrés non
jugés, avec la raison.
Le code Python en base a été mesuré et abandonné : 132 actions serveur,
zéro citant un modèle inexistant, et les 3 « modèles sans table » sont
ir.autovacuum et deux autres modèles abstraits d'Odoo.
--- EN ---
The same number means two opposite things. Zero active cron is the
expected success of a copy and a total outage on production. A report
that ignores this cries wolf over what was just requested, and stops
being read. The expectation is therefore declared, copy or live, and each
check states what it judges under either.
Two checks were DROPPED under copy after measuring: on the untouched 12
source database, 11 crons were already late and db_backup already empty —
our own update_prod_to_dev deletes them. Red would have been noise; green
a lie. They are shown unjudged, with the reason.
In-database Python was measured and dropped: 132 server actions, none
naming a missing model, and the 3 "models without a table" are
ir.autovacuum and two other Odoo abstract models.
Assisted-by: Claude Opus 5
2026-08-24 14:39:09 -04:00
|
|
|
|
# La base locale d'abord : c'est la provenance la plus directe, et
|
|
|
|
|
|
# `_analyse_select_source` du même menu range déjà la base avant la
|
|
|
|
|
|
# sauvegarde. Deux ordres différents dans un même menu se paient en
|
|
|
|
|
|
# hésitation à chaque usage.
|
|
|
|
|
|
print(f"[1] {t('A local database')}")
|
|
|
|
|
|
print(f"[2] {t('A local backup .zip')}")
|
|
|
|
|
|
print(f"[3] {t('A remote backup (https + master password)')}")
|
|
|
|
|
|
print(f"[4] {t('A live remote instance')}")
|
[ADD] analyse: ausculter une base qui n'est pas ici
Les analyses existaient ; le chemin d'AVANT manquait. La base d'un client
est dans un zip, derrière une URL, ou vivante sur un serveur.
« Restant de migration » a dû être écrit : check_migration_quality compare
les bases de PALIER et exige le journal de progression — devant une
sauvegarde isolée, ni l'un ni l'autre n'existe.
Ses compteurs évidents ont été écartés après mesure. Comparés à la base
d'ORIGINE : champs sans colonne 25 → 72, modèles sans table 90 → 158.
Vingt-cinq et quatre-vingt-dix AVANT toute migration : du bruit. Ne
restent que les constats faux en eux-mêmes, 0 avant, non nuls après —
9 langues au drapeau NULL, 68 tables m2m absentes, 414 index doublés.
Le passe-plat RPC n'accepte que la lecture. psql l'obtient du serveur ;
une session RPC n'a rien d'équivalent, et la liste blanche est donc
appliquée dans le passe-plat, pas chez l'appelant.
--- EN ---
The analyses existed; the path BEFORE them did not. A customer database
sits in a zip, behind a URL, or live on a server.
« Migration leftovers » had to be written: check_migration_quality
compares STEP databases and needs the progression log — facing a lone
backup, neither exists.
Its obvious counters were dropped after measuring. Against the ORIGINAL
database: fields with no column 25 → 72, models with no table 90 → 158.
Twenty-five and ninety BEFORE any migration: noise. Only what is wrong in
itself remains, 0 before and non-zero after — 9 languages with a NULL
flag, 68 missing m2m tables, 414 duplicated indexes.
The RPC proxy only reads. psql gets that from the server; an RPC session
has no equivalent, so the allowlist lives in the proxy, not the caller.
Assisted-by: Claude Opus 5
2026-08-24 07:21:37 -04:00
|
|
|
|
print(f"[0] {t('Back')}")
|
[FIX] todo : une invite n'écrit plus son deux-points deux fois
`click.prompt` ajoute son propre « : » à l'étiquette qu'on lui donne, et
quinze étiquettes le portaient déjà. Le menu le plus vu du logiciel demandait
donc « Commande :: », et sept invites de déploiement à distance affichaient
un deux-points suivi d'un autre.
Le suffixe est désormais explicite partout où l'étiquette ponctue elle-même,
et sa valeur se déduit de l'étiquette : rien quand elle porte déjà l'espace
qui suit, un espace quand le deux-points est nu. Un contrôle lit l'arbre et
résout les DEUX langues, le français mettant une espace avant le deux-points
là où l'anglais n'en met pas — chercher dans le texte du code ne verrait que
la clé anglaise.
--- EN ---
`click.prompt` adds its own ":" to the label it is given, and fifteen labels
already carried one. The most-seen menu of the software therefore asked
"Command:: ", and seven remote-deployment prompts showed a colon followed by
another.
The suffix is now explicit wherever the label punctuates itself, and its value
follows from the label: nothing when it already carries the trailing space, a
space when the colon is bare. A check reads the tree and resolves BOTH
languages, French putting a space before the colon where English does not —
searching the code text would only see the English key.
Assisted-by: Claude Opus 5
2026-09-09 05:32:52 -04:00
|
|
|
|
answer = click.prompt(t("Command:"), prompt_suffix=" ")
|
[ADD] analyse: ausculter une base qui n'est pas ici
Les analyses existaient ; le chemin d'AVANT manquait. La base d'un client
est dans un zip, derrière une URL, ou vivante sur un serveur.
« Restant de migration » a dû être écrit : check_migration_quality compare
les bases de PALIER et exige le journal de progression — devant une
sauvegarde isolée, ni l'un ni l'autre n'existe.
Ses compteurs évidents ont été écartés après mesure. Comparés à la base
d'ORIGINE : champs sans colonne 25 → 72, modèles sans table 90 → 158.
Vingt-cinq et quatre-vingt-dix AVANT toute migration : du bruit. Ne
restent que les constats faux en eux-mêmes, 0 avant, non nuls après —
9 langues au drapeau NULL, 68 tables m2m absentes, 414 index doublés.
Le passe-plat RPC n'accepte que la lecture. psql l'obtient du serveur ;
une session RPC n'a rien d'équivalent, et la liste blanche est donc
appliquée dans le passe-plat, pas chez l'appelant.
--- EN ---
The analyses existed; the path BEFORE them did not. A customer database
sits in a zip, behind a URL, or live on a server.
« Migration leftovers » had to be written: check_migration_quality
compares STEP databases and needs the progression log — facing a lone
backup, neither exists.
Its obvious counters were dropped after measuring. Against the ORIGINAL
database: fields with no column 25 → 72, models with no table 90 → 158.
Twenty-five and ninety BEFORE any migration: noise. Only what is wrong in
itself remains, 0 before and non-zero after — 9 languages with a NULL
flag, 68 missing m2m tables, 414 duplicated indexes.
The RPC proxy only reads. psql gets that from the server; an RPC session
has no equivalent, so the allowlist lives in the proxy, not the caller.
Assisted-by: Claude Opus 5
2026-08-24 07:21:37 -04:00
|
|
|
|
print()
|
|
|
|
|
|
if answer == "1":
|
[ADD] analyse: l'état d'une instance, lu pour l'usage qu'on en fait
Le même chiffre veut dire deux choses opposées. Zéro cron actif est le
succès attendu d'une copie et une panne totale sur une production. Un
rapport qui ignore cela crie au loup sur ce qu'on vient de demander, et
l'on cesse de le lire. L'attente est donc déclarée, copy ou live, et
chaque contrôle dit ce qu'il juge sous l'une et sous l'autre.
Deux contrôles ont été ÉCARTÉS sous copy après mesure : sur la base 12
d'origine, jamais démarrée, 11 crons étaient déjà en retard et db_backup
déjà vide — notre propre update_prod_to_dev les efface. Les afficher en
rouge aurait été du bruit ; en vert, un mensonge. Ils sont montrés non
jugés, avec la raison.
Le code Python en base a été mesuré et abandonné : 132 actions serveur,
zéro citant un modèle inexistant, et les 3 « modèles sans table » sont
ir.autovacuum et deux autres modèles abstraits d'Odoo.
--- EN ---
The same number means two opposite things. Zero active cron is the
expected success of a copy and a total outage on production. A report
that ignores this cries wolf over what was just requested, and stops
being read. The expectation is therefore declared, copy or live, and each
check states what it judges under either.
Two checks were DROPPED under copy after measuring: on the untouched 12
source database, 11 crons were already late and db_backup already empty —
our own update_prod_to_dev deletes them. Red would have been noise; green
a lie. They are shown unjudged, with the reason.
In-database Python was measured and dropped: 132 server actions, none
naming a missing model, and the 3 "models without a table" are
ir.autovacuum and two other Odoo abstract models.
Assisted-by: Claude Opus 5
2026-08-24 14:39:09 -04:00
|
|
|
|
database = self.db_manager.select_database()
|
|
|
|
|
|
return (monitoring.KIND_DATABASE, database) if database else None
|
|
|
|
|
|
if answer == "2":
|
[ADD] analyse: ausculter une base qui n'est pas ici
Les analyses existaient ; le chemin d'AVANT manquait. La base d'un client
est dans un zip, derrière une URL, ou vivante sur un serveur.
« Restant de migration » a dû être écrit : check_migration_quality compare
les bases de PALIER et exige le journal de progression — devant une
sauvegarde isolée, ni l'un ni l'autre n'existe.
Ses compteurs évidents ont été écartés après mesure. Comparés à la base
d'ORIGINE : champs sans colonne 25 → 72, modèles sans table 90 → 158.
Vingt-cinq et quatre-vingt-dix AVANT toute migration : du bruit. Ne
restent que les constats faux en eux-mêmes, 0 avant, non nuls après —
9 langues au drapeau NULL, 68 tables m2m absentes, 414 index doublés.
Le passe-plat RPC n'accepte que la lecture. psql l'obtient du serveur ;
une session RPC n'a rien d'équivalent, et la liste blanche est donc
appliquée dans le passe-plat, pas chez l'appelant.
--- EN ---
The analyses existed; the path BEFORE them did not. A customer database
sits in a zip, behind a URL, or live on a server.
« Migration leftovers » had to be written: check_migration_quality
compares STEP databases and needs the progression log — facing a lone
backup, neither exists.
Its obvious counters were dropped after measuring. Against the ORIGINAL
database: fields with no column 25 → 72, models with no table 90 → 158.
Twenty-five and ninety BEFORE any migration: noise. Only what is wrong in
itself remains, 0 before and non-zero after — 9 languages with a NULL
flag, 68 missing m2m tables, 414 duplicated indexes.
The RPC proxy only reads. psql gets that from the server; an RPC session
has no equivalent, so the allowlist lives in the proxy, not the caller.
Assisted-by: Claude Opus 5
2026-08-24 07:21:37 -04:00
|
|
|
|
path = self.db_manager.select_backup_path()
|
|
|
|
|
|
database = self._monitoring_restore(path) if path else None
|
|
|
|
|
|
return (monitoring.KIND_DATABASE, database) if database else None
|
[ADD] analyse: l'état d'une instance, lu pour l'usage qu'on en fait
Le même chiffre veut dire deux choses opposées. Zéro cron actif est le
succès attendu d'une copie et une panne totale sur une production. Un
rapport qui ignore cela crie au loup sur ce qu'on vient de demander, et
l'on cesse de le lire. L'attente est donc déclarée, copy ou live, et
chaque contrôle dit ce qu'il juge sous l'une et sous l'autre.
Deux contrôles ont été ÉCARTÉS sous copy après mesure : sur la base 12
d'origine, jamais démarrée, 11 crons étaient déjà en retard et db_backup
déjà vide — notre propre update_prod_to_dev les efface. Les afficher en
rouge aurait été du bruit ; en vert, un mensonge. Ils sont montrés non
jugés, avec la raison.
Le code Python en base a été mesuré et abandonné : 132 actions serveur,
zéro citant un modèle inexistant, et les 3 « modèles sans table » sont
ir.autovacuum et deux autres modèles abstraits d'Odoo.
--- EN ---
The same number means two opposite things. Zero active cron is the
expected success of a copy and a total outage on production. A report
that ignores this cries wolf over what was just requested, and stops
being read. The expectation is therefore declared, copy or live, and each
check states what it judges under either.
Two checks were DROPPED under copy after measuring: on the untouched 12
source database, 11 crons were already late and db_backup already empty —
our own update_prod_to_dev deletes them. Red would have been noise; green
a lie. They are shown unjudged, with the reason.
In-database Python was measured and dropped: 132 server actions, none
naming a missing model, and the 3 "models without a table" are
ir.autovacuum and two other Odoo abstract models.
Assisted-by: Claude Opus 5
2026-08-24 14:39:09 -04:00
|
|
|
|
if answer == "3":
|
[ADD] analyse: ausculter une base qui n'est pas ici
Les analyses existaient ; le chemin d'AVANT manquait. La base d'un client
est dans un zip, derrière une URL, ou vivante sur un serveur.
« Restant de migration » a dû être écrit : check_migration_quality compare
les bases de PALIER et exige le journal de progression — devant une
sauvegarde isolée, ni l'un ni l'autre n'existe.
Ses compteurs évidents ont été écartés après mesure. Comparés à la base
d'ORIGINE : champs sans colonne 25 → 72, modèles sans table 90 → 158.
Vingt-cinq et quatre-vingt-dix AVANT toute migration : du bruit. Ne
restent que les constats faux en eux-mêmes, 0 avant, non nuls après —
9 langues au drapeau NULL, 68 tables m2m absentes, 414 index doublés.
Le passe-plat RPC n'accepte que la lecture. psql l'obtient du serveur ;
une session RPC n'a rien d'équivalent, et la liste blanche est donc
appliquée dans le passe-plat, pas chez l'appelant.
--- EN ---
The analyses existed; the path BEFORE them did not. A customer database
sits in a zip, behind a URL, or live on a server.
« Migration leftovers » had to be written: check_migration_quality
compares STEP databases and needs the progression log — facing a lone
backup, neither exists.
Its obvious counters were dropped after measuring. Against the ORIGINAL
database: fields with no column 25 → 72, models with no table 90 → 158.
Twenty-five and ninety BEFORE any migration: noise. Only what is wrong in
itself remains, 0 before and non-zero after — 9 languages with a NULL
flag, 68 missing m2m tables, 414 duplicated indexes.
The RPC proxy only reads. psql gets that from the server; an RPC session
has no equivalent, so the allowlist lives in the proxy, not the caller.
Assisted-by: Claude Opus 5
2026-08-24 07:21:37 -04:00
|
|
|
|
status, path, _name = (
|
|
|
|
|
|
self.db_manager.download_database_backup_cli()
|
|
|
|
|
|
)
|
|
|
|
|
|
if status or not path or not os.path.isfile(path):
|
|
|
|
|
|
print(f"❌ {t('The download did not produce a usable file.')}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
database = self._monitoring_restore(path)
|
|
|
|
|
|
return (monitoring.KIND_DATABASE, database) if database else None
|
[ADD] analyse: l'état d'une instance, lu pour l'usage qu'on en fait
Le même chiffre veut dire deux choses opposées. Zéro cron actif est le
succès attendu d'une copie et une panne totale sur une production. Un
rapport qui ignore cela crie au loup sur ce qu'on vient de demander, et
l'on cesse de le lire. L'attente est donc déclarée, copy ou live, et
chaque contrôle dit ce qu'il juge sous l'une et sous l'autre.
Deux contrôles ont été ÉCARTÉS sous copy après mesure : sur la base 12
d'origine, jamais démarrée, 11 crons étaient déjà en retard et db_backup
déjà vide — notre propre update_prod_to_dev les efface. Les afficher en
rouge aurait été du bruit ; en vert, un mensonge. Ils sont montrés non
jugés, avec la raison.
Le code Python en base a été mesuré et abandonné : 132 actions serveur,
zéro citant un modèle inexistant, et les 3 « modèles sans table » sont
ir.autovacuum et deux autres modèles abstraits d'Odoo.
--- EN ---
The same number means two opposite things. Zero active cron is the
expected success of a copy and a total outage on production. A report
that ignores this cries wolf over what was just requested, and stops
being read. The expectation is therefore declared, copy or live, and each
check states what it judges under either.
Two checks were DROPPED under copy after measuring: on the untouched 12
source database, 11 crons were already late and db_backup already empty —
our own update_prod_to_dev deletes them. Red would have been noise; green
a lie. They are shown unjudged, with the reason.
In-database Python was measured and dropped: 132 server actions, none
naming a missing model, and the 3 "models without a table" are
ir.autovacuum and two other Odoo abstract models.
Assisted-by: Claude Opus 5
2026-08-24 14:39:09 -04:00
|
|
|
|
if answer == "4":
|
[ADD] analyse: ausculter une base qui n'est pas ici
Les analyses existaient ; le chemin d'AVANT manquait. La base d'un client
est dans un zip, derrière une URL, ou vivante sur un serveur.
« Restant de migration » a dû être écrit : check_migration_quality compare
les bases de PALIER et exige le journal de progression — devant une
sauvegarde isolée, ni l'un ni l'autre n'existe.
Ses compteurs évidents ont été écartés après mesure. Comparés à la base
d'ORIGINE : champs sans colonne 25 → 72, modèles sans table 90 → 158.
Vingt-cinq et quatre-vingt-dix AVANT toute migration : du bruit. Ne
restent que les constats faux en eux-mêmes, 0 avant, non nuls après —
9 langues au drapeau NULL, 68 tables m2m absentes, 414 index doublés.
Le passe-plat RPC n'accepte que la lecture. psql l'obtient du serveur ;
une session RPC n'a rien d'équivalent, et la liste blanche est donc
appliquée dans le passe-plat, pas chez l'appelant.
--- EN ---
The analyses existed; the path BEFORE them did not. A customer database
sits in a zip, behind a URL, or live on a server.
« Migration leftovers » had to be written: check_migration_quality
compares STEP databases and needs the progression log — facing a lone
backup, neither exists.
Its obvious counters were dropped after measuring. Against the ORIGINAL
database: fields with no column 25 → 72, models with no table 90 → 158.
Twenty-five and ninety BEFORE any migration: noise. Only what is wrong in
itself remains, 0 before and non-zero after — 9 languages with a NULL
flag, 68 missing m2m tables, 414 duplicated indexes.
The RPC proxy only reads. psql gets that from the server; an RPC session
has no equivalent, so the allowlist lives in the proxy, not the caller.
Assisted-by: Claude Opus 5
2026-08-24 07:21:37 -04:00
|
|
|
|
return self._monitoring_live()
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def _monitoring_live(self):
|
|
|
|
|
|
"""Se connecter pour de bon, ou ne pas prétendre l'être.
|
|
|
|
|
|
|
|
|
|
|
|
Une faute dans l'URL ou dans la clé ne se verrait sinon qu'à la
|
|
|
|
|
|
première analyse, et passerait pour un défaut de l'analyse.
|
|
|
|
|
|
"""
|
|
|
|
|
|
import getpass
|
|
|
|
|
|
|
|
|
|
|
|
from script.analyse import monitoring
|
|
|
|
|
|
|
|
|
|
|
|
base_url = input(t("Instance URL (ex. https://example.com): ")).strip()
|
|
|
|
|
|
if not base_url:
|
|
|
|
|
|
return None
|
|
|
|
|
|
database = input(t("Database name on that instance: ")).strip()
|
|
|
|
|
|
if not database:
|
|
|
|
|
|
return None
|
|
|
|
|
|
login = input(t("User login: ")).strip()
|
|
|
|
|
|
if not login:
|
|
|
|
|
|
return None
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(f"[1] {t('An API key')}")
|
|
|
|
|
|
print(f"[2] {t('A password')}")
|
[FIX] todo : une invite n'écrit plus son deux-points deux fois
`click.prompt` ajoute son propre « : » à l'étiquette qu'on lui donne, et
quinze étiquettes le portaient déjà. Le menu le plus vu du logiciel demandait
donc « Commande :: », et sept invites de déploiement à distance affichaient
un deux-points suivi d'un autre.
Le suffixe est désormais explicite partout où l'étiquette ponctue elle-même,
et sa valeur se déduit de l'étiquette : rien quand elle porte déjà l'espace
qui suit, un espace quand le deux-points est nu. Un contrôle lit l'arbre et
résout les DEUX langues, le français mettant une espace avant le deux-points
là où l'anglais n'en met pas — chercher dans le texte du code ne verrait que
la clé anglaise.
--- EN ---
`click.prompt` adds its own ":" to the label it is given, and fifteen labels
already carried one. The most-seen menu of the software therefore asked
"Command:: ", and seven remote-deployment prompts showed a colon followed by
another.
The suffix is now explicit wherever the label punctuates itself, and its value
follows from the label: nothing when it already carries the trailing space, a
space when the colon is bare. A check reads the tree and resolves BOTH
languages, French putting a space before the colon where English does not —
searching the code text would only see the English key.
Assisted-by: Claude Opus 5
2026-09-09 05:32:52 -04:00
|
|
|
|
genre = click.prompt(t("Command:"), prompt_suffix=" ")
|
[ADD] analyse: ausculter une base qui n'est pas ici
Les analyses existaient ; le chemin d'AVANT manquait. La base d'un client
est dans un zip, derrière une URL, ou vivante sur un serveur.
« Restant de migration » a dû être écrit : check_migration_quality compare
les bases de PALIER et exige le journal de progression — devant une
sauvegarde isolée, ni l'un ni l'autre n'existe.
Ses compteurs évidents ont été écartés après mesure. Comparés à la base
d'ORIGINE : champs sans colonne 25 → 72, modèles sans table 90 → 158.
Vingt-cinq et quatre-vingt-dix AVANT toute migration : du bruit. Ne
restent que les constats faux en eux-mêmes, 0 avant, non nuls après —
9 langues au drapeau NULL, 68 tables m2m absentes, 414 index doublés.
Le passe-plat RPC n'accepte que la lecture. psql l'obtient du serveur ;
une session RPC n'a rien d'équivalent, et la liste blanche est donc
appliquée dans le passe-plat, pas chez l'appelant.
--- EN ---
The analyses existed; the path BEFORE them did not. A customer database
sits in a zip, behind a URL, or live on a server.
« Migration leftovers » had to be written: check_migration_quality
compares STEP databases and needs the progression log — facing a lone
backup, neither exists.
Its obvious counters were dropped after measuring. Against the ORIGINAL
database: fields with no column 25 → 72, models with no table 90 → 158.
Twenty-five and ninety BEFORE any migration: noise. Only what is wrong in
itself remains, 0 before and non-zero after — 9 languages with a NULL
flag, 68 missing m2m tables, 414 duplicated indexes.
The RPC proxy only reads. psql gets that from the server; an RPC session
has no equivalent, so the allowlist lives in the proxy, not the caller.
Assisted-by: Claude Opus 5
2026-08-24 07:21:37 -04:00
|
|
|
|
secret = getpass.getpass(
|
|
|
|
|
|
t("API key: ") if genre == "1" else t("Password: ")
|
|
|
|
|
|
)
|
|
|
|
|
|
if not secret:
|
|
|
|
|
|
return None
|
|
|
|
|
|
try:
|
|
|
|
|
|
uid, version = monitoring.live_connect(
|
|
|
|
|
|
base_url, database, login, secret
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
print(f"❌ {t('Cannot connect: ')}{exc}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
print(f"✅ {t('Connected as uid')} {uid}, {t('Odoo')} {version}")
|
|
|
|
|
|
return (monitoring.KIND_LIVE, f"{base_url} · {database}")
|
|
|
|
|
|
|
|
|
|
|
|
def _monitoring_restore(self, zip_path):
|
|
|
|
|
|
"""Restaurer la sauvegarde, puis DIRE ce que la neutralisation a pris.
|
|
|
|
|
|
|
[ADD] assistant : serveur LLM reconnu, catalogue gpt, sessions locales
L'entrée « Question IA » envoyait toute question à une seule API distante, sur
un modèle figé et sans historique : aucun serveur local n'était atteignable,
et rien ne disait où partait le texte.
Deux règles gouvernent ce qui la remplace. Un port ne dit jamais qui répond —
un seul en héberge jusqu'à trois, et une famille réémet l'API d'une autre en
entier — donc l'identité se lit dans le CORPS, par une échelle ordonnée. Et
une adresse ne devient jamais du texte d'invite : le détecteur du dépôt ne
voit pas les noms d'hôte, d'où une poignée opaque à sa place.
Vérifié : 284 tests, et contre un serveur du réseau — reconnaissance,
capacités lues, conversation multi-tours, triage juste.
--- EN ---
The "AI question" entry sent every question to a single remote API, on a
frozen model and with no history: no local server was reachable, and nothing
said where the text was going.
Two rules govern what replaces it. A port never says who answers — one hosts
up to three products, and one family re-serves another's native API in full —
so identity is read from the BODY, by an ordered ladder. And an address never
becomes prompt text: the repository's detector does not see host names, hence
an opaque handle in its place.
Checked: 284 tests, and against a server on the network — recognition,
capabilities read, a multi-turn conversation, a correct triage.
Assisted-by: Claude Opus 5
2026-09-09 06:41:56 -04:00
|
|
|
|
Une base dont le nom annonce la neutralisation peut n'en porter
|
|
|
|
|
|
aucune trace : `database.is_neutralized` absent, des crons encore
|
|
|
|
|
|
actifs, un domaine de courriel toujours en place. Poser la question,
|
|
|
|
|
|
recevoir oui et ne rien vérifier reproduit exactement cette illusion
|
|
|
|
|
|
— on relit donc la base.
|
[ADD] analyse: ausculter une base qui n'est pas ici
Les analyses existaient ; le chemin d'AVANT manquait. La base d'un client
est dans un zip, derrière une URL, ou vivante sur un serveur.
« Restant de migration » a dû être écrit : check_migration_quality compare
les bases de PALIER et exige le journal de progression — devant une
sauvegarde isolée, ni l'un ni l'autre n'existe.
Ses compteurs évidents ont été écartés après mesure. Comparés à la base
d'ORIGINE : champs sans colonne 25 → 72, modèles sans table 90 → 158.
Vingt-cinq et quatre-vingt-dix AVANT toute migration : du bruit. Ne
restent que les constats faux en eux-mêmes, 0 avant, non nuls après —
9 langues au drapeau NULL, 68 tables m2m absentes, 414 index doublés.
Le passe-plat RPC n'accepte que la lecture. psql l'obtient du serveur ;
une session RPC n'a rien d'équivalent, et la liste blanche est donc
appliquée dans le passe-plat, pas chez l'appelant.
--- EN ---
The analyses existed; the path BEFORE them did not. A customer database
sits in a zip, behind a URL, or live on a server.
« Migration leftovers » had to be written: check_migration_quality
compares STEP databases and needs the progression log — facing a lone
backup, neither exists.
Its obvious counters were dropped after measuring. Against the ORIGINAL
database: fields with no column 25 → 72, models with no table 90 → 158.
Twenty-five and ninety BEFORE any migration: noise. Only what is wrong in
itself remains, 0 before and non-zero after — 9 languages with a NULL
flag, 68 missing m2m tables, 414 duplicated indexes.
The RPC proxy only reads. psql gets that from the server; an RPC session
has no equivalent, so the allowlist lives in the proxy, not the caller.
Assisted-by: Claude Opus 5
2026-08-24 07:21:37 -04:00
|
|
|
|
"""
|
|
|
|
|
|
from script.analyse import monitoring
|
|
|
|
|
|
|
|
|
|
|
|
image = self._monitoring_image_name(zip_path)
|
|
|
|
|
|
if not image:
|
|
|
|
|
|
return None
|
|
|
|
|
|
defaut = image
|
|
|
|
|
|
database = input(
|
|
|
|
|
|
f"💬 {t('Database name (default=')}{defaut}) : "
|
|
|
|
|
|
).strip()
|
|
|
|
|
|
database = database or defaut
|
|
|
|
|
|
|
|
|
|
|
|
neutralise = (
|
|
|
|
|
|
input(f"💬 {t('Neutralize the database (Y/n)? ')}").strip().lower()
|
|
|
|
|
|
)
|
|
|
|
|
|
more_arg = ""
|
|
|
|
|
|
if neutralise != "n":
|
|
|
|
|
|
more_arg = "--neutralize "
|
|
|
|
|
|
database += "_neutralize"
|
[ADD] transform data : anonymiser un fichier externe ou une base Odoo
Partager un fichier ou une base demandait d'anonymiser à la main,
colonne par colonne, ou de ne pas partager. L'entrée ouvre un fichier
externe — Excel, Access, CSV, XML, JSON — ou une base Odoo, dit ce
qu'elle porte, puis en tire une copie. L'original n'est jamais touché.
Deux règles gouvernent ce qui sort. Une colonne n'est laissée intacte
que sur son CONTENU mesuré, hors onze étiquettes structurelles : décider
sur l'étiquette recopiait les colonnes les plus identifiantes en les
annonçant protégées. Et la copie est relue octet par octet, un classeur
portant de la donnée en une douzaine d'endroits hors des cellules.
Vérifié : 5 748 tests — 13 rouges, déjà rouges sur master.
--- EN ---
Sharing a file or a database meant anonymising by hand, column by
column, or not sharing. The entry opens an external file — Excel,
Access, CSV, XML, JSON — or an Odoo database, says what it holds, then
draws a copy of it. The original is never touched.
Two rules govern what comes out. A column is left alone on its measured
CONTENT, bar eleven structural labels: deciding on the label copied the
most identifying columns verbatim while announcing them as protected.
And the copy is reread byte by byte, a workbook holding data in a dozen
places that are not cells.
Checked: 5,748 tests — 13 red, already red on master.
Assisted-by: Claude Opus 5
2026-09-10 23:58:01 -04:00
|
|
|
|
else:
|
|
|
|
|
|
# Refuser est un choix légitime — reproduire un bug de cron
|
|
|
|
|
|
# sur une copie en est un — mais il faut savoir ce que la
|
|
|
|
|
|
# copie pourra faire depuis cette machine.
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"⚠ {t('Not neutralized: this copy can send mail and run')}"
|
|
|
|
|
|
f" {t('its crons from this machine.')}"
|
|
|
|
|
|
)
|
[ADD] analyse: ausculter une base qui n'est pas ici
Les analyses existaient ; le chemin d'AVANT manquait. La base d'un client
est dans un zip, derrière une URL, ou vivante sur un serveur.
« Restant de migration » a dû être écrit : check_migration_quality compare
les bases de PALIER et exige le journal de progression — devant une
sauvegarde isolée, ni l'un ni l'autre n'existe.
Ses compteurs évidents ont été écartés après mesure. Comparés à la base
d'ORIGINE : champs sans colonne 25 → 72, modèles sans table 90 → 158.
Vingt-cinq et quatre-vingt-dix AVANT toute migration : du bruit. Ne
restent que les constats faux en eux-mêmes, 0 avant, non nuls après —
9 langues au drapeau NULL, 68 tables m2m absentes, 414 index doublés.
Le passe-plat RPC n'accepte que la lecture. psql l'obtient du serveur ;
une session RPC n'a rien d'équivalent, et la liste blanche est donc
appliquée dans le passe-plat, pas chez l'appelant.
--- EN ---
The analyses existed; the path BEFORE them did not. A customer database
sits in a zip, behind a URL, or live on a server.
« Migration leftovers » had to be written: check_migration_quality
compares STEP databases and needs the progression log — facing a lone
backup, neither exists.
Its obvious counters were dropped after measuring. Against the ORIGINAL
database: fields with no column 25 → 72, models with no table 90 → 158.
Twenty-five and ninety BEFORE any migration: noise. Only what is wrong in
itself remains, 0 before and non-zero after — 9 languages with a NULL
flag, 68 missing m2m tables, 414 duplicated indexes.
The RPC proxy only reads. psql gets that from the server; an RPC session
has no equivalent, so the allowlist lives in the proxy, not the caller.
Assisted-by: Claude Opus 5
2026-08-24 07:21:37 -04:00
|
|
|
|
|
[ADD] transform data : anonymiser un fichier externe ou une base Odoo
Partager un fichier ou une base demandait d'anonymiser à la main,
colonne par colonne, ou de ne pas partager. L'entrée ouvre un fichier
externe — Excel, Access, CSV, XML, JSON — ou une base Odoo, dit ce
qu'elle porte, puis en tire une copie. L'original n'est jamais touché.
Deux règles gouvernent ce qui sort. Une colonne n'est laissée intacte
que sur son CONTENU mesuré, hors onze étiquettes structurelles : décider
sur l'étiquette recopiait les colonnes les plus identifiantes en les
annonçant protégées. Et la copie est relue octet par octet, un classeur
portant de la donnée en une douzaine d'endroits hors des cellules.
Vérifié : 5 748 tests — 13 rouges, déjà rouges sur master.
--- EN ---
Sharing a file or a database meant anonymising by hand, column by
column, or not sharing. The entry opens an external file — Excel,
Access, CSV, XML, JSON — or an Odoo database, says what it holds, then
draws a copy of it. The original is never touched.
Two rules govern what comes out. A column is left alone on its measured
CONTENT, bar eleven structural labels: deciding on the label copied the
most identifying columns verbatim while announcing them as protected.
And the copy is reread byte by byte, a workbook holding data in a dozen
places that are not cells.
Checked: 5,748 tests — 13 red, already red on master.
Assisted-by: Claude Opus 5
2026-09-10 23:58:01 -04:00
|
|
|
|
status, _ = self.execute.exec_command_live(
|
[ADD] analyse: ausculter une base qui n'est pas ici
Les analyses existaient ; le chemin d'AVANT manquait. La base d'un client
est dans un zip, derrière une URL, ou vivante sur un serveur.
« Restant de migration » a dû être écrit : check_migration_quality compare
les bases de PALIER et exige le journal de progression — devant une
sauvegarde isolée, ni l'un ni l'autre n'existe.
Ses compteurs évidents ont été écartés après mesure. Comparés à la base
d'ORIGINE : champs sans colonne 25 → 72, modèles sans table 90 → 158.
Vingt-cinq et quatre-vingt-dix AVANT toute migration : du bruit. Ne
restent que les constats faux en eux-mêmes, 0 avant, non nuls après —
9 langues au drapeau NULL, 68 tables m2m absentes, 414 index doublés.
Le passe-plat RPC n'accepte que la lecture. psql l'obtient du serveur ;
une session RPC n'a rien d'équivalent, et la liste blanche est donc
appliquée dans le passe-plat, pas chez l'appelant.
--- EN ---
The analyses existed; the path BEFORE them did not. A customer database
sits in a zip, behind a URL, or live on a server.
« Migration leftovers » had to be written: check_migration_quality
compares STEP databases and needs the progression log — facing a lone
backup, neither exists.
Its obvious counters were dropped after measuring. Against the ORIGINAL
database: fields with no column 25 → 72, models with no table 90 → 158.
Twenty-five and ninety BEFORE any migration: noise. Only what is wrong in
itself remains, 0 before and non-zero after — 9 languages with a NULL
flag, 68 missing m2m tables, 414 duplicated indexes.
The RPC proxy only reads. psql gets that from the server; an RPC session
has no equivalent, so the allowlist lives in the proxy, not the caller.
Assisted-by: Claude Opus 5
2026-08-24 07:21:37 -04:00
|
|
|
|
f"python3 ./script/database/db_restore.py -d {database} "
|
|
|
|
|
|
f"{more_arg}--ignore_cache --image {image}",
|
|
|
|
|
|
return_status_and_output=True,
|
|
|
|
|
|
single_source_erplibre=True,
|
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
if status:
|
|
|
|
|
|
print(f"❌ {t('The restore failed.')}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
if more_arg:
|
[ADD] transform data : anonymiser un fichier externe ou une base Odoo
Partager un fichier ou une base demandait d'anonymiser à la main,
colonne par colonne, ou de ne pas partager. L'entrée ouvre un fichier
externe — Excel, Access, CSV, XML, JSON — ou une base Odoo, dit ce
qu'elle porte, puis en tire une copie. L'original n'est jamais touché.
Deux règles gouvernent ce qui sort. Une colonne n'est laissée intacte
que sur son CONTENU mesuré, hors onze étiquettes structurelles : décider
sur l'étiquette recopiait les colonnes les plus identifiantes en les
annonçant protégées. Et la copie est relue octet par octet, un classeur
portant de la donnée en une douzaine d'endroits hors des cellules.
Vérifié : 5 748 tests — 13 rouges, déjà rouges sur master.
--- EN ---
Sharing a file or a database meant anonymising by hand, column by
column, or not sharing. The entry opens an external file — Excel,
Access, CSV, XML, JSON — or an Odoo database, says what it holds, then
draws a copy of it. The original is never touched.
Two rules govern what comes out. A column is left alone on its measured
CONTENT, bar eleven structural labels: deciding on the label copied the
most identifying columns verbatim while announcing them as protected.
And the copy is reread byte by byte, a workbook holding data in a dozen
places that are not cells.
Checked: 5,748 tests — 13 red, already red on master.
Assisted-by: Claude Opus 5
2026-09-10 23:58:01 -04:00
|
|
|
|
status, _ = self.execute.exec_command_live(
|
[ADD] analyse: ausculter une base qui n'est pas ici
Les analyses existaient ; le chemin d'AVANT manquait. La base d'un client
est dans un zip, derrière une URL, ou vivante sur un serveur.
« Restant de migration » a dû être écrit : check_migration_quality compare
les bases de PALIER et exige le journal de progression — devant une
sauvegarde isolée, ni l'un ni l'autre n'existe.
Ses compteurs évidents ont été écartés après mesure. Comparés à la base
d'ORIGINE : champs sans colonne 25 → 72, modèles sans table 90 → 158.
Vingt-cinq et quatre-vingt-dix AVANT toute migration : du bruit. Ne
restent que les constats faux en eux-mêmes, 0 avant, non nuls après —
9 langues au drapeau NULL, 68 tables m2m absentes, 414 index doublés.
Le passe-plat RPC n'accepte que la lecture. psql l'obtient du serveur ;
une session RPC n'a rien d'équivalent, et la liste blanche est donc
appliquée dans le passe-plat, pas chez l'appelant.
--- EN ---
The analyses existed; the path BEFORE them did not. A customer database
sits in a zip, behind a URL, or live on a server.
« Migration leftovers » had to be written: check_migration_quality
compares STEP databases and needs the progression log — facing a lone
backup, neither exists.
Its obvious counters were dropped after measuring. Against the ORIGINAL
database: fields with no column 25 → 72, models with no table 90 → 158.
Twenty-five and ninety BEFORE any migration: noise. Only what is wrong in
itself remains, 0 before and non-zero after — 9 languages with a NULL
flag, 68 missing m2m tables, 414 duplicated indexes.
The RPC proxy only reads. psql gets that from the server; an RPC session
has no equivalent, so the allowlist lives in the proxy, not the caller.
Assisted-by: Claude Opus 5
2026-08-24 07:21:37 -04:00
|
|
|
|
f"./script/addons/update_prod_to_dev.sh {database}",
|
|
|
|
|
|
return_status_and_output=True,
|
|
|
|
|
|
single_source_erplibre=True,
|
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(
|
|
|
|
|
|
monitoring.neutralize_report(
|
|
|
|
|
|
monitoring.neutralize_state(database), colour=True
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
if status:
|
|
|
|
|
|
# Le compte test/test vient de `user_test`, posé par ce
|
|
|
|
|
|
# script. S'il n'a pas fini, l'annoncer quand même enverrait
|
|
|
|
|
|
# se heurter à un refus d'authentification.
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"⚠ {t('update_prod_to_dev did not finish: do not')}"
|
|
|
|
|
|
f" {t('count on the test/test account.')}"
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"ℹ️ {t('You can log in with test / test.')}")
|
|
|
|
|
|
return database
|
|
|
|
|
|
|
|
|
|
|
|
def _monitoring_image_name(self, zip_path):
|
|
|
|
|
|
"""db_restore veut un NOM sous image_db/, pas un chemin.
|
|
|
|
|
|
|
|
|
|
|
|
Une sauvegarde qui vient d'ailleurs n'est donc pas restaurable telle
|
|
|
|
|
|
quelle. Plutôt que de recopier plusieurs gigaoctets, on propose un
|
|
|
|
|
|
lien — et on le demande, parce que cela pose un fichier dans un
|
|
|
|
|
|
répertoire qui n'est pas à nous.
|
|
|
|
|
|
"""
|
|
|
|
|
|
image_db = os.path.join(os.getcwd(), "image_db")
|
|
|
|
|
|
nom = os.path.basename(zip_path)
|
|
|
|
|
|
if nom.endswith(".zip"):
|
|
|
|
|
|
nom = nom[:-4]
|
|
|
|
|
|
if os.path.dirname(os.path.abspath(zip_path)) == image_db:
|
|
|
|
|
|
return nom
|
|
|
|
|
|
cible = os.path.join(image_db, f"{nom}.zip")
|
|
|
|
|
|
if os.path.exists(cible):
|
|
|
|
|
|
print(f"ℹ️ {t('Using the file already in image_db: ')}{cible}")
|
|
|
|
|
|
return nom
|
|
|
|
|
|
answer = (
|
|
|
|
|
|
input(
|
|
|
|
|
|
f"💬 {t('db_restore only reads image_db/. Link it there')}"
|
|
|
|
|
|
f" ({cible}) (Y/n)? "
|
|
|
|
|
|
)
|
|
|
|
|
|
.strip()
|
|
|
|
|
|
.lower()
|
|
|
|
|
|
)
|
|
|
|
|
|
if answer == "n":
|
|
|
|
|
|
return None
|
|
|
|
|
|
try:
|
|
|
|
|
|
os.makedirs(image_db, exist_ok=True)
|
|
|
|
|
|
os.symlink(os.path.abspath(zip_path), cible)
|
|
|
|
|
|
except OSError as exc:
|
|
|
|
|
|
print(f"❌ {t('Cannot link into image_db: ')}{exc}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
print(f"🔗 {cible}")
|
|
|
|
|
|
return nom
|
|
|
|
|
|
|
2025-12-22 04:02:44 -05:00
|
|
|
|
def prompt_execute_process(self):
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(f"🤖 {t('Manage execution processes!')}")
|
2026-03-10 03:45:11 -04:00
|
|
|
|
choices = [
|
2026-03-12 05:31:45 -04:00
|
|
|
|
{"prompt_description": t("Kill Odoo process from actual port")},
|
|
|
|
|
|
{"prompt_description": t("Kill git daemon server process")},
|
2025-12-22 04:02:44 -05:00
|
|
|
|
]
|
2026-03-10 03:45:11 -04:00
|
|
|
|
help_info = self.fill_help_info(choices)
|
2025-12-22 04:02:44 -05:00
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
|
print()
|
|
|
|
|
|
if status == "0":
|
|
|
|
|
|
return False
|
|
|
|
|
|
elif status == "1":
|
|
|
|
|
|
self.process_kill_from_port()
|
2026-03-09 16:34:43 -04:00
|
|
|
|
elif status == "2":
|
|
|
|
|
|
self.process_kill_git_daemon()
|
2025-12-22 04:02:44 -05:00
|
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(t("Command not found !"))
|
2025-12-22 04:02:44 -05:00
|
|
|
|
|
2026-03-09 16:34:43 -04:00
|
|
|
|
def process_kill_git_daemon(self):
|
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
|
"pkill -f 'git daemon'",
|
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
)
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(t("Git daemon process killed."))
|
2026-03-09 16:34:43 -04:00
|
|
|
|
|
2026-03-12 02:51:01 -04:00
|
|
|
|
def prompt_execute_rtk(self):
|
2026-03-13 15:04:14 -04:00
|
|
|
|
print(
|
|
|
|
|
|
f"🤖 {t('Manage RTK (Rust Token Killer) for token optimization!')}"
|
|
|
|
|
|
)
|
2026-03-12 02:51:01 -04:00
|
|
|
|
choices = [
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
{"section": t("Setup")},
|
2026-03-12 05:31:45 -04:00
|
|
|
|
{"prompt_description": t("Install RTK")},
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
{"prompt_description": t("Initialize global auto-rewrite hook")},
|
|
|
|
|
|
{"section": t("Status")},
|
2026-03-12 05:31:45 -04:00
|
|
|
|
{"prompt_description": t("Check RTK version")},
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
{"prompt_description": t("Check RTK status")},
|
2026-03-12 05:31:45 -04:00
|
|
|
|
{"prompt_description": t("Show cumulative token savings")},
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
{"section": t("Optimize")},
|
2026-03-12 05:31:45 -04:00
|
|
|
|
{"prompt_description": t("Discover optimization opportunities")},
|
2026-03-12 02:51:01 -04:00
|
|
|
|
]
|
|
|
|
|
|
help_info = self.fill_help_info(choices)
|
|
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
|
print()
|
|
|
|
|
|
if status == "0":
|
|
|
|
|
|
return False
|
|
|
|
|
|
elif status == "1":
|
|
|
|
|
|
self.rtk_install()
|
|
|
|
|
|
elif status == "2":
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
self.rtk_init_global()
|
2026-03-12 02:51:01 -04:00
|
|
|
|
elif status == "3":
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
self.rtk_check_version()
|
2026-03-12 02:51:01 -04:00
|
|
|
|
elif status == "4":
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
self.rtk_check_status()
|
2026-03-12 02:51:01 -04:00
|
|
|
|
elif status == "5":
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
self.rtk_show_gain()
|
2026-03-12 02:51:01 -04:00
|
|
|
|
elif status == "6":
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
self.rtk_discover()
|
2026-03-12 02:51:01 -04:00
|
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(t("Command not found !"))
|
2026-03-12 02:51:01 -04:00
|
|
|
|
|
2026-09-02 03:53:40 -04:00
|
|
|
|
def rtk_locate(self):
|
|
|
|
|
|
"""Localise l'exécutable rtk.
|
|
|
|
|
|
|
|
|
|
|
|
Rend le couple (chemin, visible_dans_le_PATH). Le chemin vaut None
|
|
|
|
|
|
quand rtk est introuvable. Le second membre est faux quand le binaire
|
|
|
|
|
|
existe à l'emplacement où l'installateur le dépose sans que le PATH y
|
|
|
|
|
|
mène : un processus garde le PATH qu'il avait au démarrage, donc une
|
|
|
|
|
|
installation faite pendant que TODO tourne lui reste invisible tant
|
|
|
|
|
|
qu'il n'est pas relancé.
|
|
|
|
|
|
"""
|
|
|
|
|
|
rtk_path = shutil.which("rtk")
|
|
|
|
|
|
if rtk_path:
|
|
|
|
|
|
return rtk_path, True
|
|
|
|
|
|
# Emplacement par défaut de l'installateur (RTK_INSTALL_DIR le change).
|
|
|
|
|
|
fallback = os.path.expanduser("~/.local/bin/rtk")
|
|
|
|
|
|
if os.access(fallback, os.X_OK):
|
|
|
|
|
|
return fallback, False
|
|
|
|
|
|
return None, False
|
|
|
|
|
|
|
|
|
|
|
|
def rtk_exec(self, args):
|
|
|
|
|
|
"""Lance rtk par son chemin absolu, ou signale qu'il est absent.
|
|
|
|
|
|
|
|
|
|
|
|
Le chemin absolu évite le code 127 d'un « rtk » nu quand le PATH du
|
|
|
|
|
|
processus ne mène pas à l'emplacement d'installation.
|
|
|
|
|
|
"""
|
|
|
|
|
|
rtk_path, _ = self.rtk_locate()
|
|
|
|
|
|
if rtk_path is None:
|
|
|
|
|
|
print(t("RTK is not installed. Use option 1 to install it."))
|
|
|
|
|
|
return 1
|
|
|
|
|
|
return self.execute.exec_command_live(
|
|
|
|
|
|
f"{shlex.quote(rtk_path)} {args}",
|
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def rtk_version(self, rtk_path):
|
|
|
|
|
|
"""Rend la version qu'annonce le binaire, « ? » s'il ne répond pas."""
|
|
|
|
|
|
result = self.execute.exec_command_live(
|
|
|
|
|
|
f"{shlex.quote(rtk_path)} --version",
|
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
quiet=True,
|
|
|
|
|
|
return_status_and_output=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
if isinstance(result, tuple) and result[0] == 0:
|
|
|
|
|
|
return " ".join(result[1]).strip()
|
|
|
|
|
|
return "?"
|
|
|
|
|
|
|
|
|
|
|
|
def rtk_report_path_warning(self):
|
|
|
|
|
|
"""Dit comment rendre rtk appelable quand le PATH ne le porte pas."""
|
|
|
|
|
|
print(t("rtk is not in the PATH of this process, restart TODO."))
|
|
|
|
|
|
print(t("To make it permanent, add to your shell profile:"))
|
|
|
|
|
|
print(' export PATH="$HOME/.local/bin:$PATH"')
|
|
|
|
|
|
|
|
|
|
|
|
def rtk_report_install(self, exit_code):
|
|
|
|
|
|
"""Annonce le résultat de l'installation, PATH compris.
|
|
|
|
|
|
|
|
|
|
|
|
Une installation réussie ne rend pas rtk appelable pour autant : le
|
|
|
|
|
|
binaire atterrit dans un répertoire que le PATH du processus courant
|
|
|
|
|
|
peut ignorer. Distinguer les deux cas évite de conclure à un échec
|
|
|
|
|
|
devant un « commande introuvable » qui ne tient qu'au PATH.
|
|
|
|
|
|
"""
|
|
|
|
|
|
if exit_code:
|
|
|
|
|
|
print(f"❌ {t('RTK installation failed, see the output above.')}")
|
|
|
|
|
|
return
|
|
|
|
|
|
rtk_path, in_path = self.rtk_locate()
|
|
|
|
|
|
if rtk_path is None:
|
|
|
|
|
|
print(
|
|
|
|
|
|
"❌"
|
|
|
|
|
|
f" {t('Installation ended without error, but no rtk binary was found.')}"
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"✅ {t('RTK is installed, version: ')}{self.rtk_version(rtk_path)}"
|
|
|
|
|
|
)
|
|
|
|
|
|
print(f" {rtk_path}")
|
|
|
|
|
|
if not in_path:
|
|
|
|
|
|
self.rtk_report_path_warning()
|
|
|
|
|
|
|
2026-03-12 02:51:01 -04:00
|
|
|
|
def rtk_install(self):
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(f"🤖 {t('Installation method:')}")
|
2026-03-12 02:51:01 -04:00
|
|
|
|
choices = [
|
2026-03-12 05:31:45 -04:00
|
|
|
|
{"prompt_description": t("curl - Automatic install script")},
|
|
|
|
|
|
{"prompt_description": t("brew - Homebrew (macOS/Linux)")},
|
2026-03-13 15:04:14 -04:00
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
|
"cargo - Build from source (Rust required)"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
2026-03-12 02:51:01 -04:00
|
|
|
|
]
|
|
|
|
|
|
help_info = self.fill_help_info(choices)
|
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
|
print()
|
|
|
|
|
|
if status == "0":
|
|
|
|
|
|
return
|
|
|
|
|
|
elif status == "1":
|
2026-09-02 03:53:40 -04:00
|
|
|
|
command = (
|
|
|
|
|
|
"curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/"
|
|
|
|
|
|
"refs/heads/master/install.sh | sh"
|
2026-03-12 02:51:01 -04:00
|
|
|
|
)
|
|
|
|
|
|
elif status == "2":
|
2026-09-02 03:53:40 -04:00
|
|
|
|
command = "brew install rtk"
|
2026-03-12 02:51:01 -04:00
|
|
|
|
elif status == "3":
|
2026-09-02 03:53:40 -04:00
|
|
|
|
command = "cargo install --git https://github.com/rtk-ai/rtk"
|
2026-03-12 02:51:01 -04:00
|
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(t("Command not found !"))
|
2026-09-02 03:53:40 -04:00
|
|
|
|
return
|
|
|
|
|
|
exit_code = self.execute.exec_command_live(
|
|
|
|
|
|
command,
|
2026-03-12 02:51:01 -04:00
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
)
|
2026-09-02 03:53:40 -04:00
|
|
|
|
self.rtk_report_install(exit_code)
|
|
|
|
|
|
|
|
|
|
|
|
def rtk_check_version(self):
|
|
|
|
|
|
self.rtk_exec("--version")
|
2026-03-12 02:51:01 -04:00
|
|
|
|
|
|
|
|
|
|
def rtk_show_gain(self):
|
2026-09-02 03:53:40 -04:00
|
|
|
|
self.rtk_exec("gain")
|
2026-03-12 02:51:01 -04:00
|
|
|
|
|
|
|
|
|
|
def rtk_discover(self):
|
2026-09-02 03:53:40 -04:00
|
|
|
|
self.rtk_exec("discover")
|
2026-03-12 02:51:01 -04:00
|
|
|
|
|
|
|
|
|
|
def rtk_init_global(self):
|
2026-09-02 03:53:40 -04:00
|
|
|
|
self.rtk_exec("init --global")
|
2026-03-12 02:51:01 -04:00
|
|
|
|
|
|
|
|
|
|
def rtk_check_status(self):
|
2026-09-02 03:53:40 -04:00
|
|
|
|
rtk_path, in_path = self.rtk_locate()
|
2026-03-12 02:51:01 -04:00
|
|
|
|
if rtk_path is None:
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(t("RTK is not installed. Use option 1 to install it."))
|
2026-03-12 02:51:01 -04:00
|
|
|
|
return
|
|
|
|
|
|
|
2026-09-02 03:53:40 -04:00
|
|
|
|
print(
|
|
|
|
|
|
f"{t('RTK is installed, version: ')}{self.rtk_version(rtk_path)}"
|
2026-03-12 02:51:01 -04:00
|
|
|
|
)
|
2026-09-02 03:53:40 -04:00
|
|
|
|
print(f" {rtk_path}")
|
|
|
|
|
|
if not in_path:
|
|
|
|
|
|
self.rtk_report_path_warning()
|
2026-03-12 02:51:01 -04:00
|
|
|
|
|
|
|
|
|
|
config_path = os.path.expanduser("~/.config/rtk/config.toml")
|
|
|
|
|
|
if os.path.exists(config_path):
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(t("Global auto-rewrite hook: active"))
|
2026-03-12 02:51:01 -04:00
|
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(t("Global auto-rewrite hook: inactive"))
|
2026-03-12 02:51:01 -04:00
|
|
|
|
|
2026-02-13 03:46:27 -05:00
|
|
|
|
def prompt_execute_config(self):
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(f"🤖 {t('Manage ERPLibre and Odoo configuration!')}")
|
2026-03-10 03:45:11 -04:00
|
|
|
|
choices = [
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
{"section": t("Generate")},
|
2026-03-12 05:31:45 -04:00
|
|
|
|
{"prompt_description": t("Generate all configuration")},
|
|
|
|
|
|
{"prompt_description": t("Generate from pre-configuration")},
|
|
|
|
|
|
{"prompt_description": t("Generate from backup file")},
|
|
|
|
|
|
{"prompt_description": t("Generate from database")},
|
[ADD] todo: QEMU/KVM menu
Deploy, list, test, resize, delete and clean up VMs. Adds the SSH
configuration with recursive ProxyJump, port forwarding, and registration
of the QEMU hosts in virt-manager.
--- FR ---
Déployer, lister, tester, redimensionner, supprimer et nettoyer des VM.
Ajoute la configuration SSH avec ProxyJump récursif, la redirection de port
et l'enregistrement des hôtes QEMU dans virt-manager.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
{"section": t("Advanced")},
|
2026-03-12 05:31:45 -04:00
|
|
|
|
{"prompt_description": t("Setup queue job for parallelism")},
|
2026-02-13 03:46:27 -05:00
|
|
|
|
]
|
2026-03-10 03:45:11 -04:00
|
|
|
|
help_info = self.fill_help_info(choices)
|
2026-02-13 03:46:27 -05:00
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
|
print()
|
|
|
|
|
|
if status == "0":
|
|
|
|
|
|
return False
|
|
|
|
|
|
elif status == "1":
|
|
|
|
|
|
self.generate_config()
|
|
|
|
|
|
elif status == "2":
|
|
|
|
|
|
self.generate_config_from_preconfiguration()
|
|
|
|
|
|
elif status == "3":
|
|
|
|
|
|
self.generate_config_from_backup()
|
|
|
|
|
|
elif status == "4":
|
|
|
|
|
|
self.generate_config_from_database()
|
2026-02-28 02:14:38 -05:00
|
|
|
|
elif status == "5":
|
|
|
|
|
|
self.generate_config_queue_job()
|
2026-02-13 03:46:27 -05:00
|
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(t("Command not found !"))
|
2026-02-13 03:46:27 -05:00
|
|
|
|
|
2026-02-28 03:24:22 -05:00
|
|
|
|
def prompt_execute_network(self):
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(f"🤖 {t('Network tools!')}")
|
2026-03-10 03:45:11 -04:00
|
|
|
|
choices = [
|
2026-03-12 05:31:45 -04:00
|
|
|
|
{"prompt_description": t("SSH port-forwarding")},
|
2026-03-07 02:59:07 -05:00
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
2026-03-12 05:31:45 -04:00
|
|
|
|
"Network performance request per second"
|
2026-03-07 02:59:07 -05:00
|
|
|
|
)
|
|
|
|
|
|
},
|
[ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.
Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.
--- EN ---
`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.
The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.
Assisted-by: Claude Opus 5
2026-09-03 23:43:02 -04:00
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
|
"VPN - Tunnels (L2TP/IPsec, WireGuard, OpenVPN...)"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
[ADD] todo network: start the Odoo reverse proxy, local or network
Network gains a fourth entry that starts script/reverse_proxy/main.py
with the web and bus ports read from config.conf, falling back to the
older xmlrpc_port and longpolling_port names. It asks for this machine
only or the whole network, local by default, and warns when proxy_mode
is off or workers is 0: the first makes Odoo ignore X-Forwarded-*, the
second leaves no bus port, so /websocket fails behind the proxy too.
Checked: 48 tests pass (proxy, config reading, menu wiring, i18n); the
French screen renders with the real config.conf.
--- FR ---
[ADD] todo network : lancer le mandataire inverse Odoo, local ou réseau
Network gagne une quatrième entrée qui lance script/reverse_proxy/main.py
avec les ports web et bus lus dans config.conf, repli sur les anciens noms
xmlrpc_port et longpolling_port. Elle demande cette machine seule ou tout
le réseau, local par défaut, et prévient quand proxy_mode est éteint ou
workers vaut 0 : le premier fait ignorer X-Forwarded-* à Odoo, le second
ne laisse aucun port de bus, et /websocket échoue aussi derrière le proxy.
Vérifié : 48 tests passent (mandataire, lecture du config, câblage du
menu, i18n) ; l'écran français s'affiche avec le vrai config.conf.
Assisted-by: Claude Opus 5.5
2026-09-25 06:12:08 -04:00
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
|
"Odoo reverse proxy (pages and websocket on one port)"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
2026-02-28 03:24:22 -05:00
|
|
|
|
]
|
2026-03-10 03:45:11 -04:00
|
|
|
|
help_info = self.fill_help_info(choices)
|
2026-02-28 03:24:22 -05:00
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
|
print()
|
|
|
|
|
|
if status == "0":
|
|
|
|
|
|
return False
|
|
|
|
|
|
elif status == "1":
|
|
|
|
|
|
self.generate_network_port_forwarding()
|
|
|
|
|
|
elif status == "2":
|
|
|
|
|
|
self.generate_network_performance_test()
|
[ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.
Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.
--- EN ---
`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.
The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.
Assisted-by: Claude Opus 5
2026-09-03 23:43:02 -04:00
|
|
|
|
elif status == "3":
|
|
|
|
|
|
self.prompt_execute_vpn()
|
[ADD] todo network: start the Odoo reverse proxy, local or network
Network gains a fourth entry that starts script/reverse_proxy/main.py
with the web and bus ports read from config.conf, falling back to the
older xmlrpc_port and longpolling_port names. It asks for this machine
only or the whole network, local by default, and warns when proxy_mode
is off or workers is 0: the first makes Odoo ignore X-Forwarded-*, the
second leaves no bus port, so /websocket fails behind the proxy too.
Checked: 48 tests pass (proxy, config reading, menu wiring, i18n); the
French screen renders with the real config.conf.
--- FR ---
[ADD] todo network : lancer le mandataire inverse Odoo, local ou réseau
Network gagne une quatrième entrée qui lance script/reverse_proxy/main.py
avec les ports web et bus lus dans config.conf, repli sur les anciens noms
xmlrpc_port et longpolling_port. Elle demande cette machine seule ou tout
le réseau, local par défaut, et prévient quand proxy_mode est éteint ou
workers vaut 0 : le premier fait ignorer X-Forwarded-* à Odoo, le second
ne laisse aucun port de bus, et /websocket échoue aussi derrière le proxy.
Vérifié : 48 tests passent (mandataire, lecture du config, câblage du
menu, i18n) ; l'écran français s'affiche avec le vrai config.conf.
Assisted-by: Claude Opus 5.5
2026-09-25 06:12:08 -04:00
|
|
|
|
elif status == "4":
|
|
|
|
|
|
self.network_reverse_proxy()
|
2026-02-28 03:24:22 -05:00
|
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(t("Command not found !"))
|
2026-02-28 03:24:22 -05:00
|
|
|
|
|
|
|
|
|
|
def generate_network_port_forwarding(self, add_arg=None):
|
|
|
|
|
|
# ssh -L local_port:localhost:remote_port SSH_connection
|
|
|
|
|
|
ssh_connection = click.prompt(
|
|
|
|
|
|
"SSH connection, check ~/.ssh/config or user@address"
|
|
|
|
|
|
)
|
|
|
|
|
|
local_port = click.prompt("local port (8069)")
|
|
|
|
|
|
remote_port = click.prompt("remote port (8069)")
|
|
|
|
|
|
cmd = f"ssh -L {local_port}:localhost:{remote_port} {ssh_connection}"
|
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
|
cmd,
|
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
single_source_erplibre=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def generate_network_performance_test(self, add_arg=None):
|
|
|
|
|
|
# ./script/performance/test_performance.sh
|
|
|
|
|
|
address = click.prompt("https address, like https://erplibre.com")
|
|
|
|
|
|
cmd = f"./script/performance/test_performance.sh {address}"
|
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
|
cmd,
|
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
single_source_erplibre=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
[ADD] todo network: start the Odoo reverse proxy, local or network
Network gains a fourth entry that starts script/reverse_proxy/main.py
with the web and bus ports read from config.conf, falling back to the
older xmlrpc_port and longpolling_port names. It asks for this machine
only or the whole network, local by default, and warns when proxy_mode
is off or workers is 0: the first makes Odoo ignore X-Forwarded-*, the
second leaves no bus port, so /websocket fails behind the proxy too.
Checked: 48 tests pass (proxy, config reading, menu wiring, i18n); the
French screen renders with the real config.conf.
--- FR ---
[ADD] todo network : lancer le mandataire inverse Odoo, local ou réseau
Network gagne une quatrième entrée qui lance script/reverse_proxy/main.py
avec les ports web et bus lus dans config.conf, repli sur les anciens noms
xmlrpc_port et longpolling_port. Elle demande cette machine seule ou tout
le réseau, local par défaut, et prévient quand proxy_mode est éteint ou
workers vaut 0 : le premier fait ignorer X-Forwarded-* à Odoo, le second
ne laisse aucun port de bus, et /websocket échoue aussi derrière le proxy.
Vérifié : 48 tests passent (mandataire, lecture du config, câblage du
menu, i18n) ; l'écran français s'affiche avec le vrai config.conf.
Assisted-by: Claude Opus 5.5
2026-09-25 06:12:08 -04:00
|
|
|
|
def network_reverse_proxy(self, config_path="./config.conf"):
|
|
|
|
|
|
"""Lance script/reverse_proxy/main.py avec les ports de config_path.
|
|
|
|
|
|
|
|
|
|
|
|
Demande l'écoute — la machine seule ou tout le réseau — puis signale
|
|
|
|
|
|
les réglages d'Odoo sans lesquels le mandataire ne sert à rien :
|
|
|
|
|
|
proxy_mode (Odoo ignore sinon les en-têtes X-Forwarded-*) et workers
|
|
|
|
|
|
(à 0, aucun port de bus n'écoute et /websocket échoue). Le
|
|
|
|
|
|
mandataire tourne au premier plan ; Ctrl+C le rend au menu.
|
|
|
|
|
|
"""
|
|
|
|
|
|
from script.reverse_proxy.main import read_odoo_config
|
|
|
|
|
|
|
|
|
|
|
|
odoo = read_odoo_config(config_path)
|
|
|
|
|
|
print(f"\n{t('Listen on:')}")
|
|
|
|
|
|
print(f" [1] {t('Local only (127.0.0.1)')} *")
|
|
|
|
|
|
print(f" [2] {t('Whole network (0.0.0.0)')}")
|
|
|
|
|
|
choice = input(t("Choice (1-2, default 1): ")).strip()
|
|
|
|
|
|
listen = "0.0.0.0" if choice == "2" else "127.0.0.1"
|
|
|
|
|
|
if listen == "0.0.0.0":
|
|
|
|
|
|
reachable = t(
|
|
|
|
|
|
"The proxy is reachable by every machine on the network."
|
|
|
|
|
|
)
|
|
|
|
|
|
print(f"⚠️ {reachable}")
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"{t('Ports read from')} {config_path} : web"
|
|
|
|
|
|
f" {odoo['web_port']}, bus {odoo['websocket_port']}"
|
|
|
|
|
|
)
|
|
|
|
|
|
if not odoo["proxy_mode"]:
|
|
|
|
|
|
missing = t("is missing: Odoo ignores the X-Forwarded-* headers.")
|
|
|
|
|
|
print(f"⚠️ proxy_mode = True {missing}")
|
|
|
|
|
|
if odoo["workers"] < 1:
|
|
|
|
|
|
no_bus = t("no bus port listens, /websocket will fail.")
|
|
|
|
|
|
print(f"⚠️ workers = {odoo['workers']} : {no_bus}")
|
|
|
|
|
|
print(t("Ctrl+C stops the proxy."))
|
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
|
"./script/reverse_proxy/main.py"
|
|
|
|
|
|
f" --listen {listen}"
|
|
|
|
|
|
f" --web-port {odoo['web_port']}"
|
|
|
|
|
|
f" --websocket-port {odoo['websocket_port']}",
|
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-03-07 01:38:55 -05:00
|
|
|
|
def prompt_execute_security(self):
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(f"🤖 {t('Dependency security audit!')}")
|
2026-03-10 03:45:11 -04:00
|
|
|
|
choices = [
|
2026-03-13 15:04:14 -04:00
|
|
|
|
{
|
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
|
"pip-audit - Check vulnerabilities on Python environments"
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
2026-03-07 01:38:55 -05:00
|
|
|
|
]
|
2026-03-10 03:45:11 -04:00
|
|
|
|
help_info = self.fill_help_info(choices)
|
2026-03-07 01:38:55 -05:00
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
|
print()
|
|
|
|
|
|
if status == "0":
|
|
|
|
|
|
return False
|
|
|
|
|
|
elif status == "1":
|
|
|
|
|
|
self.execute_pip_audit()
|
|
|
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(t("Command not found !"))
|
2026-03-07 01:38:55 -05:00
|
|
|
|
|
2026-08-10 03:10:50 -04:00
|
|
|
|
def prompt_execute_test(self):
|
|
|
|
|
|
print(f"🤖 {t('Test an Odoo module on a temporary database!')}")
|
|
|
|
|
|
choices = [
|
|
|
|
|
|
{"prompt_description": t("Test a module")},
|
|
|
|
|
|
{"prompt_description": t("Test a module with code coverage")},
|
|
|
|
|
|
{"prompt_description": t("ERPLibre unit tests")},
|
|
|
|
|
|
{"prompt_description": t("Mail unit tests")},
|
|
|
|
|
|
{"prompt_description": t("Analyse unit tests")},
|
[ADD] LongTest : jusqu'à quel étage un Proxmox imbriqué tient-il
La profondeur d'imbrication praticable ne se déduit pas, elle se mesure. Une
mesure à la main a trouvé, au quatrième étage, un invité 36 fois plus lent que
le temps réel — 583 secondes d'horloge pour 16 secondes de temps invité,
chaque ligne d'ACPI prenant une seconde — puis un noyau gelé au MÊME octet
quelles que soient les ressources. Un chiffre obtenu une fois, sur une
machine, n'est pas un chiffre.
D'où trois choses.
L'algorithme, en fonctions pures. Deux ressources s'épuisent en descendant :
la mémoire, chaque étage gardant de quoi faire tourner ses propres démons, et
le disque, celui de l'enfant vivant DANS celui du parent. Une troisième se
dégrade, et elle borne le vCPU à deux au-delà du premier étage : douze ont
gelé le noyau invité, les mêmes deux avançaient. La mémoire n'est PAS bornée —
la même VM gelait au même octet avec 9 Go et avec 2 Go, donc la rogner ne
gagnerait rien et priverait l'étage du dessous. Le plan est annoncé avant
toute création, et jamais au-delà de ce qui tient.
Le garde-fou dans l'écran. Il lisait la capacité de l'HÔTE et l'offrait en
entier : sur un troisième étage à 14 cœurs, il a proposé 12 vCPU à une VM qui
n'a jamais démarré. Le nombre n'était pas absurde pour la machine ; il l'était
pour sa profondeur, que l'écran ignorait. Elle se compte maintenant sur la
chaîne de ProxyJump — un rebond par étage, et c'est nous qui écrivons ces
entrées.
Le test long, dans LongTest/ et non dans test/ : le lanceur unitaire doit
rester lançable en quelques secondes, partout, y compris sans virtualisation.
La descente est uniforme — créer, attendre le ssh, installer, redémarrer et
vérifier le noyau, remettre pmxcfs debout, contrôler le stockage — et s'arrête
au premier étage qui échoue en NOMMANT l'étape. Il envoie notre
install_proxmox.sh par scp plutôt que de laisser la VM cloner le dépôt : c'est
notre code qu'on éprouve, et un correctif absent du distant a fait revenir le
même défaut sur trois VM.
--- EN ---
The practicable nesting depth cannot be deduced, only measured. A manual
measurement found, at the fourth level, a guest 36 times slower than real time
— 583 seconds of wall clock for 16 seconds of guest time, each ACPI line
taking a second — then a kernel frozen at the SAME byte whatever the
resources. A number obtained once, on one machine, is not a number.
Hence three things.
The algorithm, in pure functions. Two resources run out going down: memory,
each level keeping what its own daemons need, and disk, the child's living
INSIDE the parent's. A third degrades, and it caps the vCPU at two beyond the
first level: twelve froze the guest kernel, the same two progressed. Memory is
NOT capped — the same VM froze at the same byte with 9 GB and with 2 GB, so
trimming it would gain nothing and starve the level below. The plan is
announced before anything is created, and never beyond what fits.
The guard in the screen. It read the HOST's capacity and offered all of it: on
a third level with 14 cores it proposed 12 vCPU to a VM that never booted. The
number was not absurd for the machine; it was for its depth, which the screen
did not know. It is now counted on the ProxyJump chain — one hop per level,
and we are the ones writing those entries.
The long test, in LongTest/ and not test/: the unit runner must stay runnable
in seconds, anywhere, including without virtualisation. The descent is uniform
— create, wait for ssh, install, reboot and check the kernel, bring pmxcfs
back, check the storage — and stops at the first level that fails, NAMING the
step. It sends our install_proxmox.sh over scp instead of letting the VM clone
the repository: it is our code being exercised, and a fix absent from the
remote made the same defect return on three VMs.
Assisted-by: Claude Opus 5
(cherry picked from commit 4f70c461330cac6f46783a60e0f33052a979fa23)
2026-08-26 06:20:52 -04:00
|
|
|
|
# Hors de la suite unitaire, et le libellé le dit : ceux-là créent
|
|
|
|
|
|
# de vraies machines et durent des heures.
|
|
|
|
|
|
{"prompt_description": t("Long tests - real VMs, hours")},
|
2026-08-10 03:10:50 -04:00
|
|
|
|
]
|
|
|
|
|
|
help_info = self.fill_help_info(choices)
|
|
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
|
print()
|
|
|
|
|
|
if status == "0":
|
|
|
|
|
|
return False
|
|
|
|
|
|
elif status == "1":
|
|
|
|
|
|
self.execute_test_module(coverage=False)
|
|
|
|
|
|
elif status == "2":
|
|
|
|
|
|
self.execute_test_module(coverage=True)
|
|
|
|
|
|
elif status == "3":
|
|
|
|
|
|
self.execute_unit_tests()
|
|
|
|
|
|
elif status == "4":
|
|
|
|
|
|
self.execute_unit_tests("test_mail*.py")
|
|
|
|
|
|
elif status == "5":
|
|
|
|
|
|
self.execute_unit_tests("test_analyse*.py")
|
[ADD] LongTest : jusqu'à quel étage un Proxmox imbriqué tient-il
La profondeur d'imbrication praticable ne se déduit pas, elle se mesure. Une
mesure à la main a trouvé, au quatrième étage, un invité 36 fois plus lent que
le temps réel — 583 secondes d'horloge pour 16 secondes de temps invité,
chaque ligne d'ACPI prenant une seconde — puis un noyau gelé au MÊME octet
quelles que soient les ressources. Un chiffre obtenu une fois, sur une
machine, n'est pas un chiffre.
D'où trois choses.
L'algorithme, en fonctions pures. Deux ressources s'épuisent en descendant :
la mémoire, chaque étage gardant de quoi faire tourner ses propres démons, et
le disque, celui de l'enfant vivant DANS celui du parent. Une troisième se
dégrade, et elle borne le vCPU à deux au-delà du premier étage : douze ont
gelé le noyau invité, les mêmes deux avançaient. La mémoire n'est PAS bornée —
la même VM gelait au même octet avec 9 Go et avec 2 Go, donc la rogner ne
gagnerait rien et priverait l'étage du dessous. Le plan est annoncé avant
toute création, et jamais au-delà de ce qui tient.
Le garde-fou dans l'écran. Il lisait la capacité de l'HÔTE et l'offrait en
entier : sur un troisième étage à 14 cœurs, il a proposé 12 vCPU à une VM qui
n'a jamais démarré. Le nombre n'était pas absurde pour la machine ; il l'était
pour sa profondeur, que l'écran ignorait. Elle se compte maintenant sur la
chaîne de ProxyJump — un rebond par étage, et c'est nous qui écrivons ces
entrées.
Le test long, dans LongTest/ et non dans test/ : le lanceur unitaire doit
rester lançable en quelques secondes, partout, y compris sans virtualisation.
La descente est uniforme — créer, attendre le ssh, installer, redémarrer et
vérifier le noyau, remettre pmxcfs debout, contrôler le stockage — et s'arrête
au premier étage qui échoue en NOMMANT l'étape. Il envoie notre
install_proxmox.sh par scp plutôt que de laisser la VM cloner le dépôt : c'est
notre code qu'on éprouve, et un correctif absent du distant a fait revenir le
même défaut sur trois VM.
--- EN ---
The practicable nesting depth cannot be deduced, only measured. A manual
measurement found, at the fourth level, a guest 36 times slower than real time
— 583 seconds of wall clock for 16 seconds of guest time, each ACPI line
taking a second — then a kernel frozen at the SAME byte whatever the
resources. A number obtained once, on one machine, is not a number.
Hence three things.
The algorithm, in pure functions. Two resources run out going down: memory,
each level keeping what its own daemons need, and disk, the child's living
INSIDE the parent's. A third degrades, and it caps the vCPU at two beyond the
first level: twelve froze the guest kernel, the same two progressed. Memory is
NOT capped — the same VM froze at the same byte with 9 GB and with 2 GB, so
trimming it would gain nothing and starve the level below. The plan is
announced before anything is created, and never beyond what fits.
The guard in the screen. It read the HOST's capacity and offered all of it: on
a third level with 14 cores it proposed 12 vCPU to a VM that never booted. The
number was not absurd for the machine; it was for its depth, which the screen
did not know. It is now counted on the ProxyJump chain — one hop per level,
and we are the ones writing those entries.
The long test, in LongTest/ and not test/: the unit runner must stay runnable
in seconds, anywhere, including without virtualisation. The descent is uniform
— create, wait for ssh, install, reboot and check the kernel, bring pmxcfs
back, check the storage — and stops at the first level that fails, NAMING the
step. It sends our install_proxmox.sh over scp instead of letting the VM clone
the repository: it is our code being exercised, and a fix absent from the
remote made the same defect return on three VMs.
Assisted-by: Claude Opus 5
(cherry picked from commit 4f70c461330cac6f46783a60e0f33052a979fa23)
2026-08-26 06:20:52 -04:00
|
|
|
|
elif status == "6":
|
|
|
|
|
|
self.prompt_execute_longtest()
|
2026-08-10 03:10:50 -04:00
|
|
|
|
else:
|
|
|
|
|
|
print(t("Command not found !"))
|
|
|
|
|
|
|
2026-08-07 03:22:26 -04:00
|
|
|
|
def execute_test_module(self, coverage=False):
|
|
|
|
|
|
# Module name
|
|
|
|
|
|
module_name = input(t("Module name to test: ")).strip()
|
|
|
|
|
|
if not module_name:
|
|
|
|
|
|
print(t("Module name is required!"))
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Database name
|
|
|
|
|
|
db_name = input(
|
|
|
|
|
|
t("Temporary database name (default: test_todo_tmp): ")
|
|
|
|
|
|
).strip()
|
|
|
|
|
|
if not db_name:
|
|
|
|
|
|
db_name = "test_todo_tmp"
|
|
|
|
|
|
|
|
|
|
|
|
# Extra modules
|
|
|
|
|
|
extra_modules = input(
|
|
|
|
|
|
t("Extra modules to install (comma-separated, empty for none): ")
|
|
|
|
|
|
).strip()
|
|
|
|
|
|
|
|
|
|
|
|
# Log level
|
|
|
|
|
|
log_level = input(t("Log level (default: test): ")).strip()
|
|
|
|
|
|
if not log_level:
|
|
|
|
|
|
log_level = "test"
|
|
|
|
|
|
|
|
|
|
|
|
# Build module list
|
|
|
|
|
|
modules_to_install = module_name
|
|
|
|
|
|
if extra_modules:
|
|
|
|
|
|
modules_to_install += f",{extra_modules}"
|
|
|
|
|
|
|
|
|
|
|
|
# Step 1: Create temp DB
|
|
|
|
|
|
print(f"\n--- {t('Creating temporary database')} '{db_name}' ---")
|
|
|
|
|
|
cmd_restore = f"./script/database/db_restore.py --database {db_name}"
|
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
|
cmd_restore,
|
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
single_source_erplibre=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Step 2: Install modules
|
|
|
|
|
|
print(f"\n--- {t('Installing modules')}: {modules_to_install} ---")
|
|
|
|
|
|
cmd_install = (
|
[ADD] déploiement : un proxy SOCKS par SSH, et son mode d'emploi
« -L » relaie UN service ; « -D » ouvre un relais SOCKS, par lequel le
navigateur atteint n'importe quelle destination depuis la machine distante —
une interface qui n'écoute que sur sa boucle locale, un hôte de son réseau sans
route depuis ici. Le port par défaut est 1080, modifiable, et contrôlé libre
avant d'ouvrir. La saisie de l'adresse est celle de sshfs, désormais partagée :
l'alias de ~/.ssh/config part tel quel à ssh, faute de quoi son ProxyJump se
perdrait et une VM imbriquée deviendrait injoignable. Le réglage de Firefox
s'affiche AVANT le lancement, la commande ne rendant la main qu'au Ctrl+C.
Vérifié : 9 tests neufs, et la numérotation du menu que deux autres gardent.
--- EN ---
"-L" relays ONE service; "-D" opens a SOCKS relay, through which the browser
reaches any destination from the remote machine — an interface listening only
on its loopback, a host of its network with no route from here. The default
port is 1080, changeable, and checked free before opening. The address prompt
is sshfs's, now shared: the ~/.ssh/config alias goes to ssh as is, failing
which its ProxyJump would be lost and a nested VM become unreachable. The
Firefox settings print BEFORE the launch, the command only returning on
Ctrl+C.
Checked: 9 new tests, and the menu numbering two others guard.
Assisted-by: Claude Opus 5
2026-09-24 13:32:28 -04:00
|
|
|
|
f"./script/addons/install_addons.sh {db_name} {modules_to_install}"
|
2026-08-07 03:22:26 -04:00
|
|
|
|
)
|
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
|
cmd_install,
|
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
single_source_erplibre=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Step 3: Run tests
|
|
|
|
|
|
print(f"\n--- {t('Running tests')}: {module_name} ---")
|
|
|
|
|
|
cmd_test = (
|
|
|
|
|
|
f"ODOO_MODE_TEST=true"
|
|
|
|
|
|
f" ./run.sh"
|
|
|
|
|
|
f" -d {db_name}"
|
|
|
|
|
|
f" -u {module_name}"
|
|
|
|
|
|
f" --log-level={log_level}"
|
|
|
|
|
|
)
|
|
|
|
|
|
if coverage:
|
|
|
|
|
|
cmd_test = f"ODOO_MODE_COVERAGE=true {cmd_test}"
|
|
|
|
|
|
status_code, output = self.execute.exec_command_live(
|
|
|
|
|
|
cmd_test,
|
|
|
|
|
|
return_status_and_output=True,
|
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
single_source_erplibre=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if status_code == 0:
|
|
|
|
|
|
print(f"\n✅ {t('Tests completed successfully!')}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"\n❌ {t('Tests failed with return code')} {status_code}")
|
|
|
|
|
|
|
|
|
|
|
|
# Step 4: Cleanup
|
|
|
|
|
|
keep_input = input(t("Keep the temporary database? (y/N): "))
|
|
|
|
|
|
keep = self._is_yes(keep_input)
|
|
|
|
|
|
if keep:
|
|
|
|
|
|
print(f"{t('Database kept')}: {db_name}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"\n--- {t('Cleaning up temporary database')} '{db_name}' ---"
|
|
|
|
|
|
)
|
|
|
|
|
|
cmd_drop = f"./odoo_bin.sh db --drop --database {db_name}"
|
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
|
cmd_drop,
|
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
single_source_erplibre=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-08-10 03:10:50 -04:00
|
|
|
|
def execute_unit_tests(self, pattern="test_*.py"):
|
|
|
|
|
|
"""Lance `unittest discover` sur un SOUS-ENSEMBLE de la suite.
|
|
|
|
|
|
|
|
|
|
|
|
Le motif est le seul paramètre : la suite complète dure plusieurs
|
|
|
|
|
|
minutes, dominées par les tests TUI montés, et attendre tout pour
|
|
|
|
|
|
vérifier un coin précis décourage de lancer les tests du tout. Une
|
|
|
|
|
|
entrée de menu supplémentaire coûte donc un motif, pas une méthode.
|
|
|
|
|
|
"""
|
|
|
|
|
|
print(f"\n--- {t('Running unit tests')} ---")
|
|
|
|
|
|
# `-u` : unittest écrit son verdict sur STDERR, les `print()` des
|
|
|
|
|
|
# tests sur STDOUT. Capturés ensemble, stderr passe sans tampon
|
|
|
|
|
|
# tandis que stdout est tamponné par blocs — tout le stdout se
|
|
|
|
|
|
# déversait donc APRÈS le « OK », qui se retrouvait noyé au milieu
|
|
|
|
|
|
# de la sortie au lieu d'en être le dernier mot. Sans tampon, les
|
|
|
|
|
|
# deux flux s'entrelacent dans l'ordre réel.
|
|
|
|
|
|
cmd = (
|
|
|
|
|
|
".venv.erplibre/bin/python -u -m unittest discover"
|
|
|
|
|
|
f" -s test -p '{pattern}' -v"
|
|
|
|
|
|
)
|
|
|
|
|
|
status_code, output = self.execute.exec_command_live(
|
|
|
|
|
|
cmd,
|
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
return_status_and_output=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
if status_code == 0:
|
|
|
|
|
|
print(f"\n✅ {t('All unit tests passed')}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"\n❌ {t('Some unit tests failed, exit code')}: {status_code}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-03-07 01:38:55 -05:00
|
|
|
|
def execute_pip_audit(self):
|
2026-03-10 03:45:11 -04:00
|
|
|
|
versions, installed_versions, odoo_installed_version = (
|
2026-03-10 03:59:35 -04:00
|
|
|
|
get_odoo_version()
|
2026-03-07 01:38:55 -05:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Build list of installed environments
|
2026-03-10 03:45:11 -04:00
|
|
|
|
environments = {}
|
2026-03-07 01:38:55 -05:00
|
|
|
|
key_i = 0
|
2026-03-10 03:45:11 -04:00
|
|
|
|
for version_info in versions[::-1]:
|
|
|
|
|
|
erplibre_version = version_info.get("erplibre_version")
|
2026-03-07 01:38:55 -05:00
|
|
|
|
venv_path = f".venv.{erplibre_version}"
|
|
|
|
|
|
req_path = f"requirement/requirements.{erplibre_version}.txt"
|
2026-03-10 03:45:11 -04:00
|
|
|
|
odoo_version = f"odoo{version_info.get('odoo_version')}"
|
2026-03-07 01:38:55 -05:00
|
|
|
|
|
|
|
|
|
|
if not os.path.isdir(venv_path):
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
key_i += 1
|
|
|
|
|
|
key_s = str(key_i)
|
|
|
|
|
|
label = f"{key_s}: {erplibre_version}"
|
|
|
|
|
|
if odoo_version == odoo_installed_version:
|
2026-03-12 05:31:45 -04:00
|
|
|
|
label += f" - {t('Current')}"
|
[FIX] script todo : lire « default » là où le fichier l'écrit ainsi
L'étiquette « - Default » ne paraissait jamais, ni au choix de version ni au
choix d'environnement : les deux lectures interrogeaient « Default » avec une
majuscule, quand conf/supported_version_erplibre.json écrit « default ».
dict.get rend None sur une clé absente, sans rien dire — la version par
défaut passait donc pour une version ordinaire, et rien ne le signalait.
Le garde-fou ne vérifie pas ce seul cas : il apparie les deux côtés, et toute
clé que le menu interroge doit exister dans le fichier. Vérifié : 4 tests,
dont deux tombent si la majuscule revient.
--- EN ---
The « - Default » label never appeared, neither on the version choice nor on
the environment one: both reads asked for « Default » with a capital, while
conf/supported_version_erplibre.json writes « default ». dict.get returns None
on a missing key without a word — the default version therefore looked like an
ordinary one, and nothing said otherwise.
The guard does not check that single case: it pairs both sides, and every key
the menu asks for must exist in the file. Checked: 4 tests, two of which fail
if the capital comes back.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
|
if version_info.get("default"):
|
2026-03-12 05:31:45 -04:00
|
|
|
|
label += f" - {t('Default')}"
|
2026-03-07 01:38:55 -05:00
|
|
|
|
|
2026-03-10 03:45:11 -04:00
|
|
|
|
environments[key_s] = {
|
2026-03-07 01:38:55 -05:00
|
|
|
|
"label": label,
|
|
|
|
|
|
"venv_path": venv_path,
|
|
|
|
|
|
"req_path": req_path,
|
|
|
|
|
|
"erplibre_version": erplibre_version,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 03:45:11 -04:00
|
|
|
|
if not environments:
|
2026-03-13 15:04:14 -04:00
|
|
|
|
print(
|
|
|
|
|
|
t(
|
|
|
|
|
|
"No installed environment found. Install an Odoo version first."
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
2026-03-07 01:38:55 -05:00
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Show selection menu
|
|
|
|
|
|
str_input = (
|
2026-03-12 05:31:45 -04:00
|
|
|
|
f"💬 {t('Choose an environment for the audit:')}\n\t"
|
2026-03-10 03:45:11 -04:00
|
|
|
|
+ "\n\t".join([v["label"] for v in environments.values()])
|
2026-03-12 05:31:45 -04:00
|
|
|
|
+ f"\n\t0: {t('Back')}"
|
|
|
|
|
|
+ f"\n{t('Select: ')}"
|
2026-03-07 01:38:55 -05:00
|
|
|
|
)
|
|
|
|
|
|
env_input = ""
|
2026-03-10 03:45:11 -04:00
|
|
|
|
while env_input not in environments and env_input != "0":
|
2026-03-07 01:38:55 -05:00
|
|
|
|
if env_input:
|
[ADD] déploiement : un proxy SOCKS par SSH, et son mode d'emploi
« -L » relaie UN service ; « -D » ouvre un relais SOCKS, par lequel le
navigateur atteint n'importe quelle destination depuis la machine distante —
une interface qui n'écoute que sur sa boucle locale, un hôte de son réseau sans
route depuis ici. Le port par défaut est 1080, modifiable, et contrôlé libre
avant d'ouvrir. La saisie de l'adresse est celle de sshfs, désormais partagée :
l'alias de ~/.ssh/config part tel quel à ssh, faute de quoi son ProxyJump se
perdrait et une VM imbriquée deviendrait injoignable. Le réglage de Firefox
s'affiche AVANT le lancement, la commande ne rendant la main qu'au Ctrl+C.
Vérifié : 9 tests neufs, et la numérotation du menu que deux autres gardent.
--- EN ---
"-L" relays ONE service; "-D" opens a SOCKS relay, through which the browser
reaches any destination from the remote machine — an interface listening only
on its loopback, a host of its network with no route from here. The default
port is 1080, changeable, and checked free before opening. The address prompt
is sshfs's, now shared: the ~/.ssh/config alias goes to ssh as is, failing
which its ProxyJump would be lost and a nested VM become unreachable. The
Firefox settings print BEFORE the launch, the command only returning on
Ctrl+C.
Checked: 9 new tests, and the menu numbering two others guard.
Assisted-by: Claude Opus 5
2026-09-24 13:32:28 -04:00
|
|
|
|
print(f"{t('Error, cannot understand value')} '{env_input}'")
|
2026-03-07 01:38:55 -05:00
|
|
|
|
env_input = input(str_input).strip()
|
|
|
|
|
|
|
|
|
|
|
|
if env_input == "0":
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2026-03-10 03:45:11 -04:00
|
|
|
|
selected = environments[env_input]
|
2026-03-07 01:38:55 -05:00
|
|
|
|
venv_path = selected["venv_path"]
|
|
|
|
|
|
req_path = selected["req_path"]
|
|
|
|
|
|
|
|
|
|
|
|
if not os.path.isfile(req_path):
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(f"{t('Dependencies file not found: ')}{req_path}")
|
2026-03-07 01:38:55 -05:00
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# TODO support bash from parameter if open gnome-terminal
|
|
|
|
|
|
cmd = f"pip-audit -r {req_path} -l;bash"
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(f"{t('Execution: ')}{cmd}")
|
2026-03-07 01:38:55 -05:00
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
|
cmd,
|
|
|
|
|
|
source_erplibre=True,
|
|
|
|
|
|
single_source_erplibre=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-02-13 03:46:27 -05:00
|
|
|
|
def generate_config(self, add_arg=None):
|
|
|
|
|
|
# Repeating to get all item before get group
|
|
|
|
|
|
cmd = (
|
2026-02-16 17:59:13 -05:00
|
|
|
|
f"./script/git/git_merge_repo_manifest.py --output .repo/local_manifests/erplibre_manifest.xml --with_OCA;"
|
2026-02-13 03:46:27 -05:00
|
|
|
|
f"./script/git/git_repo_update_group.py;"
|
|
|
|
|
|
f"./script/generate_config.sh"
|
|
|
|
|
|
)
|
|
|
|
|
|
if add_arg:
|
|
|
|
|
|
cmd += (
|
|
|
|
|
|
f";./script/git/git_repo_update_group.py {add_arg};"
|
|
|
|
|
|
f"./script/generate_config.sh"
|
|
|
|
|
|
)
|
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
|
cmd,
|
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
single_source_erplibre=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def generate_config_from_preconfiguration(self):
|
2026-03-10 03:45:11 -04:00
|
|
|
|
choices = [
|
2026-03-12 05:31:45 -04:00
|
|
|
|
{"prompt_description": t("base")},
|
|
|
|
|
|
{"prompt_description": t("base + code_generator")},
|
|
|
|
|
|
{"prompt_description": t("base + image_db")},
|
|
|
|
|
|
{"prompt_description": t("all")},
|
2026-02-13 03:46:27 -05:00
|
|
|
|
# {"prompt_description": "base + migration"},
|
|
|
|
|
|
]
|
2026-03-10 03:45:11 -04:00
|
|
|
|
help_info = self.fill_help_info(choices)
|
2026-02-13 03:46:27 -05:00
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
|
print()
|
|
|
|
|
|
if status == "0":
|
|
|
|
|
|
return False
|
|
|
|
|
|
elif status == "1":
|
|
|
|
|
|
group = "base"
|
|
|
|
|
|
str_group = f"--group {group}"
|
|
|
|
|
|
self.generate_config(add_arg=str_group)
|
|
|
|
|
|
elif status == "2":
|
|
|
|
|
|
group = "base,code_generator"
|
|
|
|
|
|
str_group = f"--group {group}"
|
|
|
|
|
|
self.generate_config(add_arg=str_group)
|
|
|
|
|
|
elif status == "3":
|
|
|
|
|
|
group = "base,image_db"
|
|
|
|
|
|
str_group = f"--group {group}"
|
|
|
|
|
|
self.generate_config(add_arg=str_group)
|
|
|
|
|
|
elif status == "4":
|
|
|
|
|
|
self.generate_config()
|
|
|
|
|
|
# elif status == "5":
|
|
|
|
|
|
# group = "base,migration"
|
|
|
|
|
|
# str_group = f"--group {group}"
|
|
|
|
|
|
# self.generate_config(add_arg=str_group)
|
|
|
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(t("Command not found !"))
|
2026-02-13 03:46:27 -05:00
|
|
|
|
|
2026-02-28 02:08:04 -05:00
|
|
|
|
def debug_ide(self):
|
2026-03-10 03:45:11 -04:00
|
|
|
|
choices = [
|
2026-03-12 05:31:45 -04:00
|
|
|
|
{"prompt_description": t("Debug todo.py")},
|
2026-02-28 02:08:04 -05:00
|
|
|
|
]
|
2026-03-10 03:45:11 -04:00
|
|
|
|
help_info = self.fill_help_info(choices)
|
2026-02-28 02:08:04 -05:00
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
|
print()
|
|
|
|
|
|
if status == "0":
|
|
|
|
|
|
return False
|
|
|
|
|
|
elif status == "1":
|
|
|
|
|
|
self.open_pycharm_file(
|
|
|
|
|
|
os.getcwd(),
|
|
|
|
|
|
os.path.join(os.getcwd(), "script/todo/todo.py"),
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(t("Command not found !"))
|
2026-02-28 02:08:04 -05:00
|
|
|
|
|
2026-02-13 03:46:27 -05:00
|
|
|
|
def generate_config_from_backup(self):
|
2026-03-10 03:59:35 -04:00
|
|
|
|
file_name = self.db_manager.open_file_image_db()
|
2026-02-13 03:46:27 -05:00
|
|
|
|
add_arg = f"--from_backup_name {file_name} --add_repo odoo18.0/addons/MathBenTech_development"
|
|
|
|
|
|
self.generate_config(add_arg=add_arg)
|
|
|
|
|
|
|
|
|
|
|
|
def generate_config_from_database(self):
|
2026-03-10 03:59:35 -04:00
|
|
|
|
database_name = self.db_manager.select_database()
|
2026-02-13 04:07:02 -05:00
|
|
|
|
str_arg = f"--database {database_name}"
|
|
|
|
|
|
self.generate_config(add_arg=str_arg)
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
2026-02-28 02:14:38 -05:00
|
|
|
|
def generate_config_queue_job(self):
|
|
|
|
|
|
cmd = "./script/config/setup_odoo_config_conf_devops.py"
|
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
|
cmd,
|
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
single_source_erplibre=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-03-10 04:05:04 -04:00
|
|
|
|
def prompt_execute_selenium_and_run_db(
|
[FIX] security: the KeePass password leaves the command line too
Same exposure as the master password, same fix. kdbx_manager put the Odoo
password straight into the web_login command; /proc/<pid>/cmdline is
readable by every user on the machine, and no downstream filter reaches
that.
The command now carries the NAME of an environment variable, never the
value. One name per entry, because several credentials go out in a single
"parallel" call and a single variable could not tell them apart.
get_extra_command_user therefore returns (fragments, variables), and the
two call sites hand the variables to exec_command_live, which already
merged an environment.
Two things found on the way. web_login re-sent config.default_password_auth
when it retried after dismissing a modal, ignoring whatever the caller had
passed -- the retry silently fell back to "admin". And install_forgejo
printed the admin password back to the terminal, hence into the install log
and any CI capture; its own header already documents the default.
A test pins the guarantee: the fragment must not contain the password.
--- FR ---
Même exposition que pour le mot de passe maître, même correctif.
kdbx_manager mettait le mot de passe Odoo directement dans la commande
web_login ; /proc/<pid>/cmdline est lisible par tout utilisateur de la
machine, et aucun filtre en aval ne l'atteint.
La commande porte désormais le NOM d'une variable d'environnement, jamais
la valeur. Un nom par entrée, car plusieurs identifiants partent dans un
seul appel « parallel » et une variable unique ne saurait les distinguer.
get_extra_command_user rend donc (fragments, variables), et les deux
appelants confient les variables à exec_command_live, qui fusionnait déjà
un environnement.
Deux trouvailles en chemin. web_login renvoyait config.default_password_auth
à la reprise après une modale, ignorant ce que l'appelant avait fourni — la
reprise retombait en silence sur « admin ». Et install_forgejo réaffichait
le mot de passe administrateur, donc dans le journal d'installation et
toute capture de CI ; son propre en-tête documente déjà le défaut.
Un test verrouille la garantie : le fragment ne doit pas porter le secret.
Assisted-by: Claude Opus 5
2026-08-23 00:25:03 -04:00
|
|
|
|
self, db_name, extra_cmd_web_login="", web_login_env=None
|
2026-03-10 04:05:04 -04:00
|
|
|
|
):
|
2026-03-10 03:45:11 -04:00
|
|
|
|
cmd_server = f"./run.sh -d {db_name};bash"
|
2026-02-13 01:27:38 -05:00
|
|
|
|
self.execute.exec_command_live(cmd_server)
|
2025-10-31 01:10:54 -04:00
|
|
|
|
cmd_client = (
|
|
|
|
|
|
f"sleep 3;./script/selenium/web_login.py{extra_cmd_web_login};bash"
|
2025-04-28 00:32:51 -04:00
|
|
|
|
)
|
[FIX] security: the KeePass password leaves the command line too
Same exposure as the master password, same fix. kdbx_manager put the Odoo
password straight into the web_login command; /proc/<pid>/cmdline is
readable by every user on the machine, and no downstream filter reaches
that.
The command now carries the NAME of an environment variable, never the
value. One name per entry, because several credentials go out in a single
"parallel" call and a single variable could not tell them apart.
get_extra_command_user therefore returns (fragments, variables), and the
two call sites hand the variables to exec_command_live, which already
merged an environment.
Two things found on the way. web_login re-sent config.default_password_auth
when it retried after dismissing a modal, ignoring whatever the caller had
passed -- the retry silently fell back to "admin". And install_forgejo
printed the admin password back to the terminal, hence into the install log
and any CI capture; its own header already documents the default.
A test pins the guarantee: the fragment must not contain the password.
--- FR ---
Même exposition que pour le mot de passe maître, même correctif.
kdbx_manager mettait le mot de passe Odoo directement dans la commande
web_login ; /proc/<pid>/cmdline est lisible par tout utilisateur de la
machine, et aucun filtre en aval ne l'atteint.
La commande porte désormais le NOM d'une variable d'environnement, jamais
la valeur. Un nom par entrée, car plusieurs identifiants partent dans un
seul appel « parallel » et une variable unique ne saurait les distinguer.
get_extra_command_user rend donc (fragments, variables), et les deux
appelants confient les variables à exec_command_live, qui fusionnait déjà
un environnement.
Deux trouvailles en chemin. web_login renvoyait config.default_password_auth
à la reprise après une modale, ignorant ce que l'appelant avait fourni — la
reprise retombait en silence sur « admin ». Et install_forgejo réaffichait
le mot de passe administrateur, donc dans le journal d'installation et
toute capture de CI ; son propre en-tête documente déjà le défaut.
Un test verrouille la garantie : le fragment ne doit pas porter le secret.
Assisted-by: Claude Opus 5
2026-08-23 00:25:03 -04:00
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
|
cmd_client, new_env=web_login_env or None
|
|
|
|
|
|
)
|
2025-04-26 17:12:38 -04:00
|
|
|
|
|
[FIX] security: the KeePass password leaves the command line too
Same exposure as the master password, same fix. kdbx_manager put the Odoo
password straight into the web_login command; /proc/<pid>/cmdline is
readable by every user on the machine, and no downstream filter reaches
that.
The command now carries the NAME of an environment variable, never the
value. One name per entry, because several credentials go out in a single
"parallel" call and a single variable could not tell them apart.
get_extra_command_user therefore returns (fragments, variables), and the
two call sites hand the variables to exec_command_live, which already
merged an environment.
Two things found on the way. web_login re-sent config.default_password_auth
when it retried after dismissing a modal, ignoring whatever the caller had
passed -- the retry silently fell back to "admin". And install_forgejo
printed the admin password back to the terminal, hence into the install log
and any CI capture; its own header already documents the default.
A test pins the guarantee: the fragment must not contain the password.
--- FR ---
Même exposition que pour le mot de passe maître, même correctif.
kdbx_manager mettait le mot de passe Odoo directement dans la commande
web_login ; /proc/<pid>/cmdline est lisible par tout utilisateur de la
machine, et aucun filtre en aval ne l'atteint.
La commande porte désormais le NOM d'une variable d'environnement, jamais
la valeur. Un nom par entrée, car plusieurs identifiants partent dans un
seul appel « parallel » et une variable unique ne saurait les distinguer.
get_extra_command_user rend donc (fragments, variables), et les deux
appelants confient les variables à exec_command_live, qui fusionnait déjà
un environnement.
Deux trouvailles en chemin. web_login renvoyait config.default_password_auth
à la reprise après une modale, ignorant ce que l'appelant avait fourni — la
reprise retombait en silence sur « admin ». Et install_forgejo réaffichait
le mot de passe administrateur, donc dans le journal d'installation et
toute capture de CI ; son propre en-tête documente déjà le défaut.
Un test verrouille la garantie : le fragment ne doit pas porter le secret.
Assisted-by: Claude Opus 5
2026-08-23 00:25:03 -04:00
|
|
|
|
def prompt_execute_selenium(
|
|
|
|
|
|
self, command=None, extra_cmd_web_login="", web_login_env=None
|
|
|
|
|
|
):
|
2026-03-10 03:45:11 -04:00
|
|
|
|
commands = []
|
2025-04-26 17:12:38 -04:00
|
|
|
|
if not command:
|
|
|
|
|
|
cmd = "./script/selenium/web_login.py"
|
|
|
|
|
|
else:
|
|
|
|
|
|
cmd = command
|
|
|
|
|
|
|
|
|
|
|
|
if type(extra_cmd_web_login) is list:
|
|
|
|
|
|
for item in extra_cmd_web_login:
|
2026-03-10 03:45:11 -04:00
|
|
|
|
commands.append(cmd + item)
|
2025-04-26 17:12:38 -04:00
|
|
|
|
else:
|
2026-03-10 03:45:11 -04:00
|
|
|
|
commands.append(cmd + extra_cmd_web_login)
|
2025-04-26 17:12:38 -04:00
|
|
|
|
|
[FIX] security: the KeePass password leaves the command line too
Same exposure as the master password, same fix. kdbx_manager put the Odoo
password straight into the web_login command; /proc/<pid>/cmdline is
readable by every user on the machine, and no downstream filter reaches
that.
The command now carries the NAME of an environment variable, never the
value. One name per entry, because several credentials go out in a single
"parallel" call and a single variable could not tell them apart.
get_extra_command_user therefore returns (fragments, variables), and the
two call sites hand the variables to exec_command_live, which already
merged an environment.
Two things found on the way. web_login re-sent config.default_password_auth
when it retried after dismissing a modal, ignoring whatever the caller had
passed -- the retry silently fell back to "admin". And install_forgejo
printed the admin password back to the terminal, hence into the install log
and any CI capture; its own header already documents the default.
A test pins the guarantee: the fragment must not contain the password.
--- FR ---
Même exposition que pour le mot de passe maître, même correctif.
kdbx_manager mettait le mot de passe Odoo directement dans la commande
web_login ; /proc/<pid>/cmdline est lisible par tout utilisateur de la
machine, et aucun filtre en aval ne l'atteint.
La commande porte désormais le NOM d'une variable d'environnement, jamais
la valeur. Un nom par entrée, car plusieurs identifiants partent dans un
seul appel « parallel » et une variable unique ne saurait les distinguer.
get_extra_command_user rend donc (fragments, variables), et les deux
appelants confient les variables à exec_command_live, qui fusionnait déjà
un environnement.
Deux trouvailles en chemin. web_login renvoyait config.default_password_auth
à la reprise après une modale, ignorant ce que l'appelant avait fourni — la
reprise retombait en silence sur « admin ». Et install_forgejo réaffichait
le mot de passe administrateur, donc dans le journal d'installation et
toute capture de CI ; son propre en-tête documente déjà le défaut.
Un test verrouille la garantie : le fragment ne doit pas porter le secret.
Assisted-by: Claude Opus 5
2026-08-23 00:25:03 -04:00
|
|
|
|
env = web_login_env or None
|
2026-03-10 03:45:11 -04:00
|
|
|
|
if len(commands) == 1:
|
[FIX] security: the KeePass password leaves the command line too
Same exposure as the master password, same fix. kdbx_manager put the Odoo
password straight into the web_login command; /proc/<pid>/cmdline is
readable by every user on the machine, and no downstream filter reaches
that.
The command now carries the NAME of an environment variable, never the
value. One name per entry, because several credentials go out in a single
"parallel" call and a single variable could not tell them apart.
get_extra_command_user therefore returns (fragments, variables), and the
two call sites hand the variables to exec_command_live, which already
merged an environment.
Two things found on the way. web_login re-sent config.default_password_auth
when it retried after dismissing a modal, ignoring whatever the caller had
passed -- the retry silently fell back to "admin". And install_forgejo
printed the admin password back to the terminal, hence into the install log
and any CI capture; its own header already documents the default.
A test pins the guarantee: the fragment must not contain the password.
--- FR ---
Même exposition que pour le mot de passe maître, même correctif.
kdbx_manager mettait le mot de passe Odoo directement dans la commande
web_login ; /proc/<pid>/cmdline est lisible par tout utilisateur de la
machine, et aucun filtre en aval ne l'atteint.
La commande porte désormais le NOM d'une variable d'environnement, jamais
la valeur. Un nom par entrée, car plusieurs identifiants partent dans un
seul appel « parallel » et une variable unique ne saurait les distinguer.
get_extra_command_user rend donc (fragments, variables), et les deux
appelants confient les variables à exec_command_live, qui fusionnait déjà
un environnement.
Deux trouvailles en chemin. web_login renvoyait config.default_password_auth
à la reprise après une modale, ignorant ce que l'appelant avait fourni — la
reprise retombait en silence sur « admin ». Et install_forgejo réaffichait
le mot de passe administrateur, donc dans le journal d'installation et
toute capture de CI ; son propre en-tête documente déjà le défaut.
Un test verrouille la garantie : le fragment ne doit pas porter le secret.
Assisted-by: Claude Opus 5
2026-08-23 00:25:03 -04:00
|
|
|
|
self.execute.exec_command_live(commands[0], new_env=env)
|
2026-03-10 03:45:11 -04:00
|
|
|
|
elif len(commands) > 1:
|
2025-04-26 17:12:38 -04:00
|
|
|
|
new_cmd = "parallel ::: "
|
2026-03-10 03:45:11 -04:00
|
|
|
|
for i, cmd in enumerate(commands):
|
2025-04-26 17:12:38 -04:00
|
|
|
|
new_cmd += f' "sleep {1 * i};{cmd}"'
|
[FIX] security: the KeePass password leaves the command line too
Same exposure as the master password, same fix. kdbx_manager put the Odoo
password straight into the web_login command; /proc/<pid>/cmdline is
readable by every user on the machine, and no downstream filter reaches
that.
The command now carries the NAME of an environment variable, never the
value. One name per entry, because several credentials go out in a single
"parallel" call and a single variable could not tell them apart.
get_extra_command_user therefore returns (fragments, variables), and the
two call sites hand the variables to exec_command_live, which already
merged an environment.
Two things found on the way. web_login re-sent config.default_password_auth
when it retried after dismissing a modal, ignoring whatever the caller had
passed -- the retry silently fell back to "admin". And install_forgejo
printed the admin password back to the terminal, hence into the install log
and any CI capture; its own header already documents the default.
A test pins the guarantee: the fragment must not contain the password.
--- FR ---
Même exposition que pour le mot de passe maître, même correctif.
kdbx_manager mettait le mot de passe Odoo directement dans la commande
web_login ; /proc/<pid>/cmdline est lisible par tout utilisateur de la
machine, et aucun filtre en aval ne l'atteint.
La commande porte désormais le NOM d'une variable d'environnement, jamais
la valeur. Un nom par entrée, car plusieurs identifiants partent dans un
seul appel « parallel » et une variable unique ne saurait les distinguer.
get_extra_command_user rend donc (fragments, variables), et les deux
appelants confient les variables à exec_command_live, qui fusionnait déjà
un environnement.
Deux trouvailles en chemin. web_login renvoyait config.default_password_auth
à la reprise après une modale, ignorant ce que l'appelant avait fourni — la
reprise retombait en silence sur « admin ». Et install_forgejo réaffichait
le mot de passe administrateur, donc dans le journal d'installation et
toute capture de CI ; son propre en-tête documente déjà le défaut.
Un test verrouille la garantie : le fragment ne doit pas porter le secret.
Assisted-by: Claude Opus 5
2026-08-23 00:25:03 -04:00
|
|
|
|
# « parallel » hérite de l'environnement, et chaque entrée lit
|
|
|
|
|
|
# SA variable : un nom par identifiant, d'où EL_WEB_LOGIN_PWD_N.
|
|
|
|
|
|
self.execute.exec_command_live(new_cmd, new_env=env)
|
2025-04-26 17:12:38 -04:00
|
|
|
|
|
2025-08-07 06:09:37 -04:00
|
|
|
|
def crash_diagnostic(self, e):
|
2026-03-10 03:06:07 -04:00
|
|
|
|
# TODO show message at start if os.path.exists(ERROR_LOG_PATH)
|
|
|
|
|
|
if os.path.exists(ERROR_LOG_PATH) and not os.path.exists(
|
|
|
|
|
|
VENV_ERPLIBRE
|
2025-08-07 06:09:37 -04:00
|
|
|
|
):
|
|
|
|
|
|
print("Got error : ")
|
|
|
|
|
|
print(e)
|
2026-03-10 03:06:07 -04:00
|
|
|
|
print("Got error at first execution.", ERROR_LOG_PATH)
|
2025-08-07 06:09:37 -04:00
|
|
|
|
try:
|
2026-03-10 03:06:07 -04:00
|
|
|
|
file = open(ERROR_LOG_PATH, "r")
|
2025-08-07 06:09:37 -04:00
|
|
|
|
content = file.read()
|
|
|
|
|
|
# TODO si vide, ajouter notre erreur
|
|
|
|
|
|
print(content)
|
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
|
print("Error: File not found.")
|
|
|
|
|
|
finally:
|
|
|
|
|
|
if "file" in locals() and file:
|
|
|
|
|
|
file.close()
|
|
|
|
|
|
# Force auto installation
|
|
|
|
|
|
print("Auto installation")
|
|
|
|
|
|
time.sleep(0.5)
|
|
|
|
|
|
cmd = "./script/todo/source_todo.sh"
|
|
|
|
|
|
# self.restart_script(e)
|
2026-02-13 01:27:38 -05:00
|
|
|
|
self.execute.exec_command_live(cmd, source_erplibre=True)
|
2025-08-07 06:09:37 -04:00
|
|
|
|
sys.exit(1)
|
2026-03-10 03:06:07 -04:00
|
|
|
|
if os.path.exists(VENV_ERPLIBRE):
|
2025-08-07 06:09:37 -04:00
|
|
|
|
print("Import error : ")
|
|
|
|
|
|
print(e)
|
|
|
|
|
|
# TODO auto-detect gnome-terminal, or choose another. Is it done already?
|
|
|
|
|
|
self.restart_script(e)
|
|
|
|
|
|
# self.prompt_install()
|
|
|
|
|
|
|
|
|
|
|
|
# print(
|
2026-03-10 03:06:07 -04:00
|
|
|
|
# f"You forgot to activate source \nsource ./{VENV_ERPLIBRE}/bin/activate"
|
2025-08-07 06:09:37 -04:00
|
|
|
|
# )
|
|
|
|
|
|
# time.sleep(0.5)
|
|
|
|
|
|
# cmd = "./script/todo/source_todo.sh"
|
|
|
|
|
|
print("Re-execute TODO 🤖 or execute :")
|
|
|
|
|
|
print()
|
2026-03-10 03:06:07 -04:00
|
|
|
|
print(f"source {VENV_ERPLIBRE}/bin/activate;make")
|
2025-08-07 06:09:37 -04:00
|
|
|
|
print()
|
|
|
|
|
|
cmd = "./script/todo/todo.py"
|
|
|
|
|
|
# # self.restart_script(e)
|
|
|
|
|
|
try:
|
|
|
|
|
|
# TODO duplicate
|
|
|
|
|
|
import click
|
|
|
|
|
|
import humanize
|
|
|
|
|
|
import openai
|
2025-10-31 01:10:54 -04:00
|
|
|
|
import urwid
|
2025-08-07 06:09:37 -04:00
|
|
|
|
from pykeepass import PyKeePass
|
|
|
|
|
|
except ImportError:
|
|
|
|
|
|
print("Rerun and exit")
|
[FIX] démarrage : choisir un interpréteur capable de lire TODO
install.sh et make lançaient todo.py par son hashbang, donc par le python3 du
PATH, sans rien garantir de sa version : une syntaxe plus récente l'arrête en
SyntaxError au chargement, et le poste hors d'âge perd tout moyen de lancer
l'installation qui l'en sortirait. L'ordre est désormais le venv d'outillage,
sinon le python3 du PATH s'il suffit, sinon l'installation. TODO lui-même se
relance dans le venv, vérifie version et modules avant ses imports, et propose
l'installation en terminal — ce que « make » sur un clone neuf promet depuis
toujours sans le tenir.
Vérifié : venv à jour, venv périmé, systèmes en 3.10, 3.14 et 3.15 vont chacun
où il faut ; un import du module ne relance rien et lève comme avant.
--- EN ---
install.sh and make ran todo.py through its hashbang, so through the python3 of
the PATH, with no guarantee about its version: a newer syntax stops it with a
SyntaxError at load, and the ageing machine loses every way to start the
install that would rescue it. The order is now the tools venv, else the python3
of the PATH when it suffices, else the install. TODO itself relaunches in the
venv, checks version and modules before its imports, and offers the install in
a terminal — what "make" on a fresh clone has always promised without
keeping.
Checked: fresh venv, stale venv, systems on 3.10, 3.14 and 3.15 each go where
they should; importing the module relaunches nothing and raises as before.
Assisted-by: Claude Opus 5
2026-09-24 13:32:16 -04:00
|
|
|
|
if _in_venv(): # la relance tournerait dans ce même venv
|
|
|
|
|
|
sys.exit(1)
|
2026-02-13 01:27:38 -05:00
|
|
|
|
self.execute.exec_command_live(cmd, source_erplibre=True)
|
2025-08-07 06:09:37 -04:00
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
print("No error")
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.prompt_install()
|
|
|
|
|
|
|
2026-02-13 04:07:02 -05:00
|
|
|
|
def open_shell_on_database(self):
|
2026-03-10 03:59:35 -04:00
|
|
|
|
database = self.db_manager.select_database()
|
2026-03-07 02:59:07 -05:00
|
|
|
|
if database:
|
|
|
|
|
|
cmd_server = f"./odoo_bin.sh shell -d {database}"
|
2026-03-10 03:45:11 -04:00
|
|
|
|
status, databases = self.execute.exec_command_live(
|
2026-03-07 02:59:07 -05:00
|
|
|
|
cmd_server,
|
|
|
|
|
|
return_status_and_output=True,
|
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
single_source_erplibre=True,
|
|
|
|
|
|
new_window=True,
|
|
|
|
|
|
)
|
2026-02-13 04:07:02 -05:00
|
|
|
|
|
2026-02-28 02:08:04 -05:00
|
|
|
|
def open_pycharm_file(self, folder, filename):
|
|
|
|
|
|
cmd = "~/.local/share/JetBrains/Toolbox/scripts/pycharm"
|
|
|
|
|
|
# cmd = "/snap/bin/pycharm-community"
|
|
|
|
|
|
# if pycharm_arg:
|
|
|
|
|
|
# cmd += f" {pycharm_arg}"
|
|
|
|
|
|
if folder:
|
|
|
|
|
|
cmd += f" {folder}"
|
|
|
|
|
|
if filename:
|
|
|
|
|
|
cmd += f" --line 1 {filename}"
|
|
|
|
|
|
self.execute.exec_command_live(cmd, source_erplibre=False)
|
|
|
|
|
|
|
2025-10-31 01:10:54 -04:00
|
|
|
|
def upgrade_module(self):
|
|
|
|
|
|
upgrade = todo_upgrade.TodoUpgrade(self)
|
|
|
|
|
|
upgrade.execute_module_upgrade()
|
|
|
|
|
|
|
2026-08-07 03:22:26 -04:00
|
|
|
|
def upgrade_poetry(self):
|
|
|
|
|
|
# Only show the version to the user
|
|
|
|
|
|
status = self.execute.exec_command_live(
|
|
|
|
|
|
f"make version",
|
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
# TODO maybe autodetect to update it
|
|
|
|
|
|
git_repo_update_input = input(
|
|
|
|
|
|
"💬 Would you like to fetch all your git repositories, you need it (y/Y) : "
|
|
|
|
|
|
)
|
|
|
|
|
|
if self._is_yes(git_repo_update_input):
|
|
|
|
|
|
status = self.execute.exec_command_live(
|
|
|
|
|
|
f"./script/manifest/update_manifest_local_dev.sh",
|
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
poetry_lock = "./poetry.lock"
|
|
|
|
|
|
try:
|
|
|
|
|
|
os.remove(poetry_lock)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
pass
|
|
|
|
|
|
odoo_long_version = ""
|
|
|
|
|
|
if os.path.exists("./.erplibre-version"):
|
|
|
|
|
|
with open("./.erplibre-version") as f:
|
|
|
|
|
|
odoo_long_version = f.read()
|
|
|
|
|
|
path_file_odoo_lock = f"./requirement/poetry.{odoo_long_version}.lock"
|
|
|
|
|
|
if odoo_long_version:
|
|
|
|
|
|
try:
|
|
|
|
|
|
os.remove(path_file_odoo_lock)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
status = self.execute.exec_command_live(
|
|
|
|
|
|
f"pip install -r requirement/erplibre_require-ments-poetry.txt && "
|
|
|
|
|
|
f"./script/poetry/poetry_update.py -f",
|
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
single_source_erplibre=False,
|
|
|
|
|
|
single_source_odoo=True,
|
|
|
|
|
|
source_odoo=odoo_long_version,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if os.path.exists(poetry_lock):
|
|
|
|
|
|
shutil.copy2(poetry_lock, path_file_odoo_lock)
|
|
|
|
|
|
|
2026-03-10 03:45:11 -04:00
|
|
|
|
def callback_execute_custom_database(self, config):
|
2026-03-10 03:59:35 -04:00
|
|
|
|
database_name = self.db_manager.select_database()
|
2026-02-28 01:35:32 -05:00
|
|
|
|
self.prompt_execute_selenium_and_run_db(database_name)
|
|
|
|
|
|
|
2025-12-22 04:02:44 -05:00
|
|
|
|
def process_kill_from_port(self):
|
|
|
|
|
|
cfg = configparser.ConfigParser()
|
|
|
|
|
|
cfg.read("./config.conf")
|
|
|
|
|
|
http_port = cfg.getint("options", "http_port")
|
|
|
|
|
|
|
2026-02-13 01:27:38 -05:00
|
|
|
|
status = self.execute.exec_command_live(
|
2025-12-22 04:02:44 -05:00
|
|
|
|
f"./script/process/kill_process_by_port.py {http_port} --kill-tree --nb_parent 2",
|
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-08-07 06:09:37 -04:00
|
|
|
|
def restart_script(self, last_error):
|
2026-03-12 05:31:45 -04:00
|
|
|
|
print(f"🤖 {t('Reboot TODO ...')}")
|
2025-08-07 06:09:37 -04:00
|
|
|
|
# os.execv(sys.executable, ['python'] + sys.argv)
|
|
|
|
|
|
# TODO mettre check que le répertoire est créé, s'il existe, auto-loop à corriger
|
2026-03-10 03:06:07 -04:00
|
|
|
|
if os.path.exists(VENV_ERPLIBRE) and not os.path.exists(
|
|
|
|
|
|
ERROR_LOG_PATH
|
2025-08-07 06:09:37 -04:00
|
|
|
|
):
|
|
|
|
|
|
# TODO mettre check import suivant ne vont pas planter
|
|
|
|
|
|
try:
|
2026-03-10 03:06:07 -04:00
|
|
|
|
with open(ERROR_LOG_PATH, "w") as f_file:
|
2025-08-07 06:09:37 -04:00
|
|
|
|
f_file.write(str(last_error))
|
|
|
|
|
|
pass # The file is created and closed here, no content is written
|
|
|
|
|
|
print(
|
2026-03-10 03:06:07 -04:00
|
|
|
|
f"Try to reopen process with before :\nsource ./{VENV_ERPLIBRE}/bin/activate && exec python "
|
2025-08-07 06:09:37 -04:00
|
|
|
|
+ " ".join(sys.argv)
|
|
|
|
|
|
)
|
|
|
|
|
|
os.execv(
|
|
|
|
|
|
"/bin/bash",
|
|
|
|
|
|
[
|
|
|
|
|
|
"/bin/bash",
|
|
|
|
|
|
"-c",
|
2026-03-10 03:06:07 -04:00
|
|
|
|
f"source ./{VENV_ERPLIBRE}/bin/activate && exec python "
|
2025-08-07 06:09:37 -04:00
|
|
|
|
+ " ".join(sys.argv),
|
|
|
|
|
|
],
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print("Error detect at first execution.")
|
|
|
|
|
|
print(e)
|
|
|
|
|
|
|
2025-10-31 01:10:54 -04:00
|
|
|
|
def on_dir_selected(self, dir_path):
|
|
|
|
|
|
self.dir_path = dir_path
|
|
|
|
|
|
todo_file_browser.exit_program()
|
|
|
|
|
|
|
2026-08-07 03:22:26 -04:00
|
|
|
|
def callback_make_mobile_home(self, config):
|
|
|
|
|
|
# Read file
|
|
|
|
|
|
default_project_name = "ERPLibre"
|
|
|
|
|
|
default_package_name = "ca.erplibre.home"
|
|
|
|
|
|
# Read default information
|
|
|
|
|
|
if os.path.exists(STRINGS_FILE):
|
|
|
|
|
|
tree = ET.parse(STRINGS_FILE)
|
|
|
|
|
|
root = tree.getroot()
|
|
|
|
|
|
for elem in root.findall("string"):
|
|
|
|
|
|
if elem.get("name") == "app_name":
|
|
|
|
|
|
default_project_name = elem.text
|
|
|
|
|
|
if elem.get("name") == "package_name":
|
|
|
|
|
|
default_package_name = elem.text
|
|
|
|
|
|
|
|
|
|
|
|
default_project_url_name = "https://erplibre.ca"
|
|
|
|
|
|
# Read default information
|
|
|
|
|
|
dotenv_file = dotenv.find_dotenv(
|
|
|
|
|
|
filename=os.path.join(MOBILE_HOME_PATH, "src", ".env.production")
|
|
|
|
|
|
)
|
|
|
|
|
|
default_project_url_name = dotenv.get_key(
|
|
|
|
|
|
dotenv_file, "VITE_WEBSITE_URL"
|
|
|
|
|
|
)
|
|
|
|
|
|
default_project_note_subject = dotenv.get_key(
|
|
|
|
|
|
dotenv_file, "VITE_LABEL_NOTE"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
default_debug = False
|
|
|
|
|
|
project_name = default_project_name
|
|
|
|
|
|
project_url_name = default_project_url_name
|
|
|
|
|
|
project_principal_subject = default_project_note_subject
|
|
|
|
|
|
package_name = default_package_name
|
|
|
|
|
|
do_debug = default_debug
|
|
|
|
|
|
do_change_picture_menu = False
|
|
|
|
|
|
|
|
|
|
|
|
do_personalize = input(
|
|
|
|
|
|
"Do you want to personalize the mobile application (Y) : "
|
|
|
|
|
|
)
|
|
|
|
|
|
if self._is_yes(do_personalize):
|
|
|
|
|
|
project_name = (
|
|
|
|
|
|
input(
|
|
|
|
|
|
f'Your project name (Separate by space in title), default "{default_project_name}" : '
|
|
|
|
|
|
).strip()
|
|
|
|
|
|
or default_project_name
|
|
|
|
|
|
)
|
|
|
|
|
|
package_name = (
|
|
|
|
|
|
input(
|
|
|
|
|
|
f'Your package name (separate by . lower case, 3 works like DOMAIN.NAME.OBJECT), default "{default_package_name}" : '
|
|
|
|
|
|
).strip()
|
|
|
|
|
|
or default_package_name
|
|
|
|
|
|
)
|
|
|
|
|
|
project_url_name = (
|
|
|
|
|
|
input(
|
|
|
|
|
|
f'Your project url website, default "{default_project_url_name}" : '
|
|
|
|
|
|
).strip()
|
|
|
|
|
|
or default_project_url_name
|
|
|
|
|
|
)
|
|
|
|
|
|
project_principal_subject = (
|
|
|
|
|
|
input(
|
|
|
|
|
|
f'Your project subject, default "{default_project_note_subject}" : '
|
|
|
|
|
|
).strip()
|
|
|
|
|
|
or default_project_note_subject
|
|
|
|
|
|
)
|
|
|
|
|
|
do_debug = self._is_yes(
|
|
|
|
|
|
input("Compilation with debug information, default No (Y) : ")
|
|
|
|
|
|
)
|
|
|
|
|
|
do_change_picture_menu = self._is_yes(
|
|
|
|
|
|
input(
|
|
|
|
|
|
"Want to change picture from menu, you need"
|
|
|
|
|
|
" android-studio (Y) : "
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Rename with script bash
|
|
|
|
|
|
cmd_client = f'cd {MOBILE_HOME_PATH} && npx cap init "{project_name}" "{package_name}" && ./rename_android.sh "{project_name}" "{package_name}" && npx cap sync android'
|
|
|
|
|
|
self.execute.exec_command_live(cmd_client, source_erplibre=False)
|
|
|
|
|
|
|
|
|
|
|
|
# dotenv_mobile = dotenv.dotenv_values(dotenv_file)
|
|
|
|
|
|
# dotenv_mobile["VITE_TITLE"] = project_name
|
|
|
|
|
|
# dotenv_mobile["VITE_WEBSITE_URL"] = project_url_name
|
|
|
|
|
|
dotenv.set_key(
|
|
|
|
|
|
dotenv_file, "VITE_TITLE", project_name, quote_mode="always"
|
|
|
|
|
|
)
|
|
|
|
|
|
dotenv.set_key(
|
|
|
|
|
|
dotenv_file,
|
|
|
|
|
|
"VITE_WEBSITE_URL",
|
|
|
|
|
|
project_url_name,
|
|
|
|
|
|
quote_mode="always",
|
|
|
|
|
|
)
|
|
|
|
|
|
dotenv.set_key(
|
|
|
|
|
|
dotenv_file,
|
|
|
|
|
|
"VITE_LABEL_NOTE",
|
|
|
|
|
|
project_principal_subject,
|
|
|
|
|
|
quote_mode="always",
|
|
|
|
|
|
)
|
|
|
|
|
|
dotenv.set_key(
|
|
|
|
|
|
dotenv_file,
|
|
|
|
|
|
"VITE_DEBUG_DEV",
|
|
|
|
|
|
"true" if do_debug else "false",
|
|
|
|
|
|
quote_mode="never",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if do_change_picture_menu:
|
|
|
|
|
|
status = self.execute.exec_command_live(
|
|
|
|
|
|
f"cd {MOBILE_HOME_PATH} && npx cap open android;bash",
|
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
|
new_window=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
print(
|
|
|
|
|
|
"Guide for Android-Studio, wait loading is finish. Right-click to app/New/Image Asset and load your image."
|
|
|
|
|
|
)
|
|
|
|
|
|
input(
|
|
|
|
|
|
"Did you finish to update image with Android-Studio ? Press to continue ..."
|
|
|
|
|
|
)
|
|
|
|
|
|
cmd_client = "cp ./mobile/erplibre_home_mobile/android/app/src/main/ic_launcher-playstore.png ./mobile/erplibre_home_mobile/src/assets/company_logo.png"
|
|
|
|
|
|
self.execute.exec_command_live(cmd_client, source_erplibre=False)
|
|
|
|
|
|
cmd_client = "cp ./mobile/erplibre_home_mobile/android/app/src/main/ic_launcher-playstore.png ./mobile/erplibre_home_mobile/src/assets/imgs/logo.png"
|
|
|
|
|
|
self.execute.exec_command_live(cmd_client, source_erplibre=False)
|
|
|
|
|
|
|
|
|
|
|
|
status = self.execute.exec_command_live(
|
|
|
|
|
|
"./mobile/compile_and_run.sh", source_erplibre=False
|
|
|
|
|
|
)
|
2026-08-10 03:10:50 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
start_time = time.time()
|
|
|
|
|
|
try:
|
|
|
|
|
|
todo = TODO()
|
|
|
|
|
|
if ENABLE_CRASH:
|
|
|
|
|
|
todo.crash_diagnostic(CRASH_E)
|
|
|
|
|
|
todo.run()
|
|
|
|
|
|
except (KeyboardInterrupt, click.exceptions.Abort):
|
|
|
|
|
|
# click.prompt() raises Abort (not a KeyboardInterrupt subclass) on
|
|
|
|
|
|
# both Ctrl+C and Ctrl+D/EOF. run() only catches it for its own
|
|
|
|
|
|
# top-level prompt; every submenu's click.prompt() would otherwise
|
|
|
|
|
|
# let Abort escape here as an uncaught exception.
|
|
|
|
|
|
print(t("Keyboard interrupt"))
|
|
|
|
|
|
finally:
|
|
|
|
|
|
end_time = time.time()
|
|
|
|
|
|
duration_sec = end_time - start_time
|
|
|
|
|
|
if humanize:
|
|
|
|
|
|
duration_delta = datetime.timedelta(seconds=duration_sec)
|
|
|
|
|
|
humain_time = humanize.precisedelta(duration_delta)
|
|
|
|
|
|
print(f"\n{t('TODO execution time')} {humain_time}\n")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"\n{t('TODO execution time')} {duration_sec:.2f} sec.\n")
|