Commit Graph

4 Commits

Author SHA1 Message Date
Florian
5917e1ca04 feat: ajouter le split terminal (plusieurs panneaux par onglet)
Layouts : 1 (normal), 2 verticaux (cote a cote), 2 horizontaux (empiles), 4
panneaux (grille 2x2), via des QSplitter imbriques (app/ui/split_pane.py).
Chaque panneau reutilise tel quel un widget d'onglet existant
(SessionTabWidget, LocalTerminalTabWidget) : aucune modification de ces
classes, seule une fine barre (menu ☰ + fermeture) les enveloppe.

- Menu contextuel d'un onglet (SSH ou terminal local, pas SFTP, pas
  d'imbrication de splits) : "Diviser" -> choix du layout. Le contenu actuel
  occupe le premier panneau, les autres recoivent un terminal local par
  defaut.
- Menu ☰ d'un panneau : changer son contenu (autre terminal local), ou
  "Deplacer un onglet ici" pour y faire migrer un onglet de premier niveau
  existant sans fermer sa connexion.
- Fermer un panneau le retire de son QSplitter (qui redistribue l'espace tout
  seul) ; s'il n'en reste qu'un, l'onglet redevient un onglet normal
  (redescente automatique, pas de chrome de split residuel).
- Fermer l'onglet divise (bouton x) ferme toutes les connexions des panneaux.
- BroadcastManager (Multi-Send) atteint desormais aussi les panneaux a
  l'interieur d'un split (MainWindow._iter_tabs les deplie).

Bug intercepte par les tests avant integration : reparenter le widget d'origine
dans son panneau retire automatiquement son onglet du QTabWidget (un widget ne
peut avoir qu'un seul parent), ce qui decale les onglets suivants ; un appel
explicite a removeTab() ensuite visait donc le mauvais onglet. Corrige en
laissant Qt gerer ce retrait implicitement plutot que de le dupliquer.

tests/test_split_terminal.py : SSHConnection.start()/LocalTerminalConnection.start()
neutralisees (teste l'agencement des panneaux et le cycle de vie des onglets,
pas une connexion reelle).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 02:35:39 +02:00
Florian
c2bde19f59 feat: ajouter le support ProxyJump / bastion (jump host)
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>
2026-09-05 02:02:25 +02:00
Florian
c08037a638 feat: ajouter la prise en charge de ~/.ssh/config
Support minimum : Host, HostName, User, Port, IdentityFile, ProxyJump,
ForwardAgent, ServerAliveInterval, ServerAliveCountMax.

- app/openssh/config.py : load_openssh_hosts() lit ~/.ssh/config via
  paramiko.SSHConfig (gere nativement les blocs "Host *" fusionnes, les alias
  multiples sur une meme ligne Host, et l'expansion ~ des IdentityFile).
  Fichier absent = liste vide, pas une erreur. session_from_openssh_host()
  convertit un Host en Session PySSH (groupe "OpenSSH", cle si IdentityFile
  presente sinon authentification auto).
- Nouvelle section "Configuration OpenSSH" dans le panneau lateral
  (SessionController), sous Sessions SSH : double-clic ou menu contextuel pour
  se connecter directement ou importer comme session (editeur pre-rempli,
  ajustable avant enregistrement) ; rafraichissement via le menu ⚙ ou le menu
  contextuel de l'hote.
- ConnectionController.open_openssh_host() ouvre une connexion directe sans
  enregistrer de session, comme une connexion rapide.

Le ProxyJump est detecte, affiche (badge "· via <bastion>", tooltip) et reporte
dans les notes d'une session importee, mais pas encore applique automatiquement
a la connexion : ce sera l'objet de la Phase 3 (Bastion / Jump Host), qui
prendra en charge toutes ses sources (OpenSSH, session PySSH, config manuelle)
via un mecanisme commun.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 01:49:59 +02:00
Florian
47005b1e26 refactor: decouper main_window.py en controllers/services/ui
main_window.py concentrait la fenetre principale, l'arbre des sessions, les 4
classes d'onglets, la detection de commandes ssh et le multi-send : 983 lignes
et une seule classe qui pilotait toute l'application.

- app/services/ssh_target.py : detection d'une cible ssh dans un terminal (pur,
  sans dependance Qt)
- app/ui/tabs.py : BaseTerminalTab / SessionTabWidget / SFTPTabWidget /
  LocalTerminalTabWidget (deplaces tels quels)
- app/controllers/session_controller.py : panneau lateral (arbre sessions +
  terminaux locaux) et CRUD des sessions
- app/controllers/connection_controller.py : cycle de vie des onglets
  (ouverture, connexion rapide, renommage, fermeture)
- app/controllers/broadcast_manager.py : bouton et logique du mode Multi-Send

main_window.py ne fait plus que construire ces composants et les relier par
signaux (983 -> ~200 lignes). Comportement inchange ; MainWindow.tabs et
detect_ssh_target restent importables depuis app.main_window pour la
compatibilite avec les tests existants.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 01:42:31 +02:00