Bibliotheque de commandes organisee par categories (Linux/Docker/Proxmox
fournis en exemples au premier lancement, librement modifiables), avec
recherche.
- app/snippets.py : Snippet/SnippetStore, meme pattern que les autres stores
(JSON atomique, schema_version).
- SnippetManagerDialog : creer/modifier/supprimer/rechercher, regroupe par
categorie.
- Integration terminal : le dernier commit du depot a delibrement fait du
clic droit un raccourci de collage (style terminal Unix), sans menu
contextuel. Plutot que d'ecraser ce choix recent, les snippets passent par
un bouton dedie "📋 Snippets" dans la barre de l'onglet (SessionTabWidget et
LocalTerminalTabWidget), avec un menu Categorie -> Snippet -> Action
(Inserer dans le terminal / Executer / Executer avec Multi-Send).
"Inserer" n'envoie jamais Entree (le texte est ecrit, a l'utilisateur de
valider) ; seul "Executer" l'ajoute.
- BroadcastManager.broadcast_now() : diffusion explicite et ponctuelle vers
tous les terminaux, independante de l'etat du bouton Multi (utilisee par
"Executer avec Multi-Send", qui est une action volontaire et pas une frappe
clavier a relayer).
- La construction du menu (_build_snippets_menu) est separee de son
affichage (_show_snippets_menu) : testable sans declencher l'appel
bloquant menu.exec().
tests/test_snippets.py : stockage (CRUD, recherche, categories, persistance)
et integration terminal (inserer/executer/multi-send, construction et
declenchement du menu), avec LocalTerminalConnection.start() neutralisee.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Un workspace capture les onglets ouverts (sessions SSH, terminaux locaux,
splits avec leurs tailles) et les restaure plus tard. Ne reference les
sessions que par id : aucun mot de passe stocke, redemande a la restauration
exactement comme une ouverture normale.
- app/workspace.py : Workspace/WorkspaceTab/WorkspaceItem + WorkspaceStore
(meme pattern que SessionStore/KeyManager : JSON atomique, schema_version).
- app/controllers/workspace_manager.py : capture() lit l'etat reel des onglets
de ConnectionController (types de widgets, layout et tailles d'un
SplitContainer) ; restore() rouvre chaque onglet via
ConnectionController.build_session_tab()/build_sftp_tab()/new_local_terminal()
(factorises hors de open_session()/open_sftp_session() pour etre reutilisables
ici) puis reconstruit les splits avec split_tab(). Un element non restaurable
(session supprimee depuis) est signale (compteur "ignores") plutot que de
faire echouer toute la restauration.
- WorkspaceDialog : creer / enregistrer (ecraser) / ouvrir / renommer /
supprimer, plus la case "Restaurer automatiquement le dernier workspace
ouvert au demarrage" (desactivee par defaut).
- app/app_settings.py : preferences globales (theme, taille de police,
dernier workspace) consolidees dans un seul module au lieu d'un mecanisme
de lecture/ecriture duplique par prefernce dans theme.py.
- SplitContainer expose layout_kind et get_layout_sizes()/set_layout_sizes()
(parcours recursif des QSplitter imbriques) pour la capture/restauration
fidele d'une grille 2x2.
tests/test_workspace.py : capture (simple + split), sauvegarde/rechargement
JSON, restauration complete et partielle, renommer/supprimer/re-enregistrer,
restauration automatique - avec app_settings.SETTINGS_FILE redirige vers un
fichier temporaire pour ne jamais toucher les vrais reglages de l'utilisateur.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Session gagne keepalive_enabled/interval/count_max (actif par defaut : ne
fait qu'envoyer des paquets de maintien, sans risque pour les sessions
existantes) et reconnect_enabled/initial_delay/max_attempts (desactive par
defaut : change reellement le comportement en cas de coupure, donc a
activer explicitement).
- SSHConnection._keepalive_loop() : thread dedie qui envoie
"keepalive@openssh.com" (comme OpenSSH) avec accuse de reception toutes les
`keepalive_interval` secondes. Un serveur conforme repond immediatement
(meme en echec), ce qui confirme que le transport est vivant. Apres
`keepalive_count_max` echecs consecutifs (aucune reponse), le canal est
ferme pour que la boucle de lecture principale s'en apercoive et emette
`disconnected` avec une raison explicite.
- SessionTabWidget programme une reconnexion (QTimer, jamais de blocage de
l'interface) quand la session l'autorise et que la deconnexion n'est pas
volontaire (_closing) : delai en backoff exponentiel plafonne
(app.services.reconnect.compute_backoff_delay), compte a rebours affiche
("Connexion perdue (...). Reconnexion dans Ns... (Tentative X/Y)"), abandon
au-dela de reconnect_max_attempts (0 = illimite), compteur remis a zero des
qu'une connexion reussit. Ne se declenche jamais sur une simple erreur
d'authentification (seul un `disconnected` apres connexion reussie
declenche une reconnexion, pas un `error` de connexion initiale - sinon un
mot de passe errone entrainerait des tentatives repetees).
- BaseTerminalTab : les signaux connected/disconnected/error d'une connexion
sont desormais associes a la generation de connexion en cours au moment ou
ils sont cables ; un signal parvenant apres qu'une reconnexion (manuelle ou
automatique) ait deja demarre une nouvelle connexion est ignore. Necessaire
ici : les signaux Qt entre threads sont mis en file et ne sont livres
qu'apres le retour de reconnect() a la boucle d'evenements, alors qu'une
nouvelle connexion est deja en place.
- SessionDialog : sections "Keepalive" et "Reconnexion automatique".
tests/test_keepalive.py (paramiko entierement mocke), test_reconnect_backoff.py
(fonction pure) et test_auto_reconnect.py (SSHConnection.start() neutralisee :
la programmation de la reconnexion ne depend d'aucune connexion reseau reelle).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Les redirections -L et -R existaient deja ; il manquait -D, ou le port local
ouvert se comporte comme un proxy SOCKS dont la destination est negociee par
chaque client (navigateur, curl...) plutot que fixee a l'avance.
- app/tunnels.py : implementation minimale d'un serveur SOCKS4/4a/5 (CONNECT
uniquement, sans authentification - largement suffisant pour un usage
local/personnel). Reutilise le meme _ForwardServer et le meme _relay() que
-L/-R ; seul le handshake initial differe (negocie la destination au lieu
de la recevoir en config). DIRECTION_DYNAMIC s'integre dans describe() et
Tunnel.stop() sans code specifique supplementaire.
- TunnelDialog : nouvelle option "Dynamique - SOCKS (-D)" ; les champs
Machine/Port de destination se masquent automatiquement pour ce mode
(QFormLayout.setRowVisible), puisque la destination n'est pas fixee ici.
tests/test_socks_tunnel.py : verifie SOCKS5 (adresse IPv4) et SOCKS4a (nom de
domaine) avec un vrai client SOCKS et un transport SSH mocke - a d'ailleurs
revele un bug reel (decode("idna", errors="replace") leve une exception, le
codec idna ne supportant pas errors="replace") sur le chemin nom-de-domaine,
corrige ici (decode("ascii", errors="replace"), les noms d'hote SOCKS etant
de l'ASCII simple).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Une session peut desormais passer par un jump host avant d'atteindre sa
cible, avec 3 sources possibles (limite a un seul saut pour l'instant) :
une session PySSH existante, un Host de ~/.ssh/config, ou une configuration
manuelle (hote/port/utilisateur/authentification/cle).
- session_manager.py : BastionConfig (mode + parametres), champ
Session.bastion. Jamais de mot de passe stocke : un bastion en mode "mot
de passe" le redemande a la connexion, comme le fait deja Session.auth_type.
- app/bastion.py : resolve_bastion_target() convertit un BastionConfig en
parametres de connexion concrets quelle que soit sa source, et leve une
erreur claire si la reference est cassee (session supprimee, alias OpenSSH
introuvable, config manuelle incomplete) plutot que d'echouer plus tard
avec un message obscur.
- ssh_connection.py : la logique de connexion (auth, cle d'hote modifiee,
mot de passe de secours en mode auto) est factorisee dans _connect_host(),
reutilisee a l'identique pour le bastion et pour la cible.
_open_bastion_channel() connecte d'abord le bastion puis ouvre un canal
direct-tcpip vers la cible, passe en sock= a la connexion finale (mecanisme
ProxyJump standard de paramiko). Le bastion a son propre SSHClient, ferme
proprement a la fin de la connexion.
- SessionDialog : section "Bastion / Jump Host" (choix de la source + champs
associes, caches/affiches via un QStackedWidget).
- ConnectionController resout le bastion et demande son mot de passe (si
nécessaire) avant d'ouvrir l'onglet ; erreur affichee immediatement si la
reference est invalide plutot que de lancer une connexion vouee a l'echec.
- app/openssh/config.py : un Host avec ProxyJump produit maintenant une
Session dont le bastion reference cet alias (mode BASTION_OPENSSH) : le
scenario proxmox-prod / bastion de la Phase 2 fonctionne de bout en bout.
tests/test_bastion.py (resolution des 3 sources + erreurs, et ouverture du
canal via un bastion entierement mocke, sans reseau reel) et
tests/test_session_dialog_bastion.py (UI : construction, pre-remplissage a
l'edition, resultat par mode).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Les info-bulles suffisent : le texte fixe sous le formulaire allongeait la
fenetre sans rien apporter de plus.
L'explication reste accessible de deux façons : en survolant une entree de la
liste deroulante, et en survolant le champ lui-meme, dont l'info-bulle reprend
desormais celle du type selectionne. Sans cette seconde info-bulle, l'explication
n'aurait ete visible qu'en ouvrant la liste.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Choisir entre Ed25519, trois courbes NIST et RSA n'a de sens que si l'on sait
ce qu'elles impliquent. Chaque type dispose desormais d'un resume, affiche a la
fois en info-bulle dans la liste deroulante et sous le formulaire, mis a jour a
chaque changement de selection.
Chaque resume indique la courbe (ou l'absence de courbe pour RSA), l'equivalence
de robustesse avec RSA selon les recommandations du NIST (SP 800-57), la
compatibilite OpenSSH, et le cas d'usage : Ed25519 par defaut, ECDSA quand une
politique de conformite impose les courbes NIST, RSA pour du materiel ancien.
La reserve sur l'origine jamais expliquee des parametres des courbes NIST est
mentionnee, puisqu'elle motive le choix d'Ed25519 par defaut.
Le champ de taille en bits, qui ne concerne que RSA, est desormais active par la
meme fonction : un seul endroit gere la reaction au changement de type.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le panneau des sessions etait declare Movable|Floatable mais pas Closable : il
n'avait donc aucun bouton de fermeture et ne pouvait pas etre masque. Sa largeur
plancher etait par ailleurs imposee par son contenu, ce qui empechait de le
retrecir vraiment.
- ajout de DockWidgetClosable, largeur minimale ramenee a 120 px
- bascule par F9, par le menu Affichage, ou par la croix du panneau, les trois
restant synchronises
- separateur elargi a 6 px et surligne au survol, pour etre attrape facilement
Generation de cles : ajout d'ECDSA sur les courbes NIST P-256, P-384 et P-521,
en plus d'Ed25519 et RSA. Ed25519 reste le choix par defaut : c'est aussi une
courbe elliptique (Curve25519), plus courte et plus rapide, et sans les
interrogations qui entourent le choix des parametres des courbes NIST. Chaque
type est verifie relisible par paramiko, donc utilisable pour se connecter.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tunnels (redirection de ports), la specialite de Bitvise : un bouton dans la
barre d'un onglet SSH ouvre un gestionnaire de redirections.
- locale (-L) : un port de votre poste est relaye, via la connexion SSH, vers
une machine joignable depuis le serveur (typiquement un service qui n'ecoute
que sur la boucle locale du serveur)
- distante (-R) : l'inverse
Chaque tunnel tourne dans son propre thread, un relais bloque ne genant ni
l'interface ni les autres tunnels. Ils sont fermes avec la session.
Envoi simultane, façon MobaXterm : le bouton "Multi" diffuse la frappe vers
tous les onglets terminal. La diffusion ecrit directement dans les connexions
et non via les widgets terminal, sinon chaque onglet rediffuserait a son tour.
Reparation : un de mes scripts de correction avait reecrit main_window.py en
retraduisant deux fois les fins de ligne, produisant des sequences \r\r\n ;
le fichier avait ete committe ainsi (2538 lignes au lieu de 856, une ligne vide
inseree partout). Le contenu etait intact, seule la mise en forme etait touchee.
Le fichier est reconstitue par normalisation des fins de ligne, et un controle
d'integrite a ete ajoute a la verification de non-regression.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reunit en un seul outil les fonctions utiles de MobaXterm, Termius et Bitvise,
sans compte ni service tiers, avec toutes les donnees en local.
- Terminaux multi-onglets : sessions SSH et terminaux Windows locaux
(PowerShell / cmd) via la pseudo-console native ConPTY
- Gestionnaire de fichiers SFTP facon WinSCP : double panneau, colonnes
triables, glisser-deposer, renommage, permissions, file de transferts.
Ouvrable sur une connexion en cours sans creer de favori
- Gestion des cles SSH : generation Ed25519/RSA, import, export
- Gestion du fichier known_hosts, avec remplacement guide d'une cle d'hote
modifiee (cas frequent des serveurs en DHCP)
- Trois themes (clair, bleu nuit, noir), commutables a chaud
- Authentification automatique reprenant le comportement de la commande ssh
(agent SSH et cles ~/.ssh), mot de passe demande seulement en secours
Aucun mot de passe n'est stocke sur le disque.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>