aa890c584711f3406d24eb004e072a3b255e9366
9 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
aa890c5847 |
SFTP sur cette connexion : ne plus perdre la cible ssh detectee
Signale par l'utilisateur avec une capture d'ecran : bouton "SFTP sur cette connexion" clique depuis un terminal local visiblement connecte en ssh (prompt root@debian:~#), formulaire de connexion vide a l'arrivee - "si je dois remplir le formulaire pour une session actuelle ca perd tout son sens". Cause reelle : detect_ssh_target() ne scrutait le texte du terminal qu'au moment du clic (buffer visible + historique pyte, 5000 lignes). Si la ligne `ssh user@hote` tapee par l'utilisateur n'y figure plus au moment du clic (beaucoup de sortie depuis, ecran efface...), plus rien n'est detectable - alors meme que la connexion ssh est toujours active. La capture montree correspond exactement a ce cas : un ecran efface apres la connexion. Corrige a la racine plutot qu'en agrandissant simplement l'historique scrute : app/ui/tabs.py (BaseTerminalTab) memorise desormais la cible des qu'une commande ssh apparait dans la sortie recue (_track_ssh_target, cablee comme terminal.feed sur data_received), et ne l'oublie plus jamais - detect_current_ssh_target() la renvoie en priorite, avec un dernier examen du buffer actuel seulement en repli. Le bouton "SFTP sur cette connexion" et le raccourci global Ctrl+Shift+F (ConnectionController._current_tab_ssh_target) utilisent tous les deux ce nouveau point d'entree unique. tests/test_ssh_target_tracking.py (4 verifications, nouveau fichier - ce bouton n'avait encore aucun test dedie) : memorisation a la frappe, cible toujours renvoyee meme quand le buffer live ne la contient plus, mise a jour vers une commande ssh plus recente, bouton emettant bien la cible memorisee. Suite complete revalidee (36/36), ruff propre. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
a65b86e25b |
Assistant IA : se rabattre sur l'ecran visible si rien n'est selectionne
Signale par l'utilisateur : "Expliquer cette sortie" n'avait aucun effet
utilisable, le menu restait grise. Cause reelle : ces actions exigeaient une
selection prealable dans le terminal, alors que l'attente naturelle ("explique
ce que j'ai a l'ecran") est de pouvoir les utiliser directement sur ce qui est
affiche - confirme avec l'utilisateur avant d'implementer (deux options
proposees : rabattre sur l'ecran visible, ou juste clarifier le blocage).
app/terminal_widget.py : nouvelle methode visible_text() (l'ecran actuellement
affiche, sans l'historique de scroll - au contraire de buffer_text() qui sert
a la detection de commande ssh et remonte l'historique proche).
app/ui/tabs.py : Expliquer/Diagnostiquer/Proposer une solution/Poser une
question utilisent desormais _ai_source_text() = la selection si elle existe
(pour cibler precisement), sinon l'ecran visible. Les 3 premieres actions du
menu ne sont plus desactivees des qu'il n'y a pas de selection, mais
seulement si vraiment rien n'est affiche ; "Poser une question" reste
utilisable meme un terminal vide (contexte optionnel, comme le bouton
"Nouvelle question" du panneau).
tests/test_ai_tab_integration.py : reecriture des tests 1 et 2 (etats
d'activation du menu) + 2 nouveaux tests verifiant explicitement le repli
(sans selection, le texte envoye est bien celui de l'ecran) et sa priorite
inverse (une selection existante prime toujours sur l'ecran visible).
tests/test_clipboard.py : couverture directe de visible_text() (ecran complet,
lignes vides de fin retirees). Suite complete revalidee (34/34), ruff propre.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
c39ef9c038 |
Qualite : pyproject.toml (ruff/pytest/pyright) + CI Gitea (lint/tests/build)
pyproject.toml devient le point d'entree unique de configuration des outils
(pytest.ini supprime, sa config y est deplacee). Les dependances restent dans
requirements.txt/requirements-dev.txt (deja utilises par setup.bat) plutot que
dupliquees ici : ce projet se distribue par PyInstaller, pas par "pip install".
ruff : configure puis passe sur tout app/ + tests/ (154 corrections
automatiques, purement mecaniques - tri des imports, imports multiples sur une
ligne separes, alias depreciees) suivies de 7 corrections manuelles ciblees :
- app/ssh_connection.py : `raise ... from None` pour le refus explicite de
passphrase (ce n'est pas un echec a diagnostiquer).
- app/controllers/workspace_manager.py : zip() sans strict= documente en
commentaire (deliberement tolerant a un workspace legerement perime).
- tests/test_ai_redaction.py : nom de variable ambigu renomme.
- E702 (construction compacte "widget = X(); widget.setY(...)" dans
sftp_panel.py/tunnel_dialog.py) et E402 (quelques tests qui importent un
symbole juste avant la section qui l'utilise) : ignores explicitement, ce
sont des styles deliberes preexistants, pas des oublis.
`ruff check app main.py tests` est desormais propre.
pyright : mode "basic" (le code n'a jamais ete type). Plus de 200 signalements
sur ~300 etaient le meme faux positif connu de l'ecosysteme PySide6 (acces
plat aux enums Qt, ex. Qt.UserRole, valide a l'execution mais que les stubs ne
reconnaissent que sous leur forme imbriquee) : desactive avec justification en
commentaire. Reste un socle de 77 signalements reels (surtout des acces
Optional non filtres dans app/ui/tabs.py et app/ssh_connection.py) : non
corrige dans cette phase - resserrer le typage la ou pyright a raison est un
travail a part, pas une consequence automatique de l'ajout de l'outil. La CI
le lance en mode informatif (continue-on-error) tant que ce socle n'est pas
resorbe.
.gitea/workflows/ci.yml : lint (ruff, bloquant), tests (pytest, bloquant),
typecheck (pyright, informatif), build (PyInstaller, sur main uniquement,
artefact du dossier portable). Necessite un runner Gitea Actions Windows
enregistre - PySide6/pywinpty/Credential Manager/ACL NTFS sont Windows
uniquement, comme l'application elle-meme ; non verifiable depuis ce poste.
Suite complete revalidee apres coup (33/33) pour confirmer qu'aucune des 154
corrections automatiques n'a modifie de comportement.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
0b2ff38bfd |
Assistant IA optionnel (analyse de sortie, diagnostic, generation de commande)
Nouvelle couche app/ai/ (provider OpenAI-compatible via urllib, redaction de secrets, detection de commandes dangereuses, parametres, worker QThread) et son integration dans chaque onglet terminal via un bouton "Assistant IA" (memes conventions que le bouton Snippets existant) : - Expliquer cette sortie / Diagnostiquer cette erreur / Proposer une solution / Poser une question... : utilisent la selection courante du terminal, affichees dans un panneau lateral (memoire uniquement, jamais journalise) reutilisant le splitter comme le panneau SFTP. - Generer une commande : ne s'execute jamais automatiquement (Copier / Inserer dans le terminal sans Entree / Annuler), avec avertissement si la commande proposee correspond a un motif destructeur connu. - Avant tout envoi : avis unique (persist) puis detection de secrets evidents (mot de passe, cle, jeton) avec choix de masquer, envoyer tel quel ou annuler. Desactive par defaut, sans liste de fournisseur figee (URL + modele libres, compatible OpenAI/OpenRouter/LLM local). La cle API passe exclusivement par CredentialStore (jamais dans settings.json ni journalisee). Parametrable via le nouveau menu "Assistant IA > Parametres..." (bouton "Tester la connexion"). Bug trouve par les tests avant commit : un panneau nouvellement insere dans un QSplitter deja visible est affiche automatiquement par Qt, ce qui inversait le premier clic sur "Afficher le panneau IA" ; corrige en le masquant explicitement a sa creation. 9 nouveaux fichiers de test (provider, redaction, dangers, parametres, worker, confirmations de securite, dialogue de commande, panneau, integration onglet) : 39 verifications, toutes au vert. Suite complete (31 fichiers) executee sans regression. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
ff4a73e6e6 |
feat: ajouter la recherche rapide Ctrl+K (Quick Connect)
Palette globale recherchant a travers sessions, hosts OpenSSH, workspaces, snippets et actions courantes, avec recherche floue et navigation entierement au clavier (fleches, Entree, Echap). - app/services/fuzzy.py : correspondance par sous-sequence (comme fzf, sans dependance externe), score favorisant les caracteres consecutifs et les debuts de mot. - QuickConnectDialog : collecte les resultats depuis les stores/controleurs deja existants (aucune duplication de logique - activer une session appelle ConnectionController.open_session(), un workspace WorkspaceManager.restore(), un snippet insert_snippet() dans l'onglet courant, sans jamais l'executer automatiquement). Les actions (Nouvelle session, Gestionnaires...) reutilisent les methodes des controleurs/dialogues existants. - Ctrl+K (menu Fichier > Recherche rapide...) ouvre la palette, reconstruite a chaque ouverture pour refleter l'etat courant. - insert_snippet()/execute_snippet() rendus publics sur BaseTerminalTab (utilises depuis un autre module par la palette). tests/test_fuzzy.py (scoring, tri par pertinence) et test_quick_connect.py (collecte des 4 types de resultats + actions, filtrage, navigation clavier, activation reelle d'une session et d'un snippet). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
0a07ddcb62 |
feat: ajouter les Snippets de commandes
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> |
||
|
|
9c8017cebd |
feat: ajouter le keepalive SSH et la reconnexion automatique
- 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> |
||
|
|
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> |
||
|
|
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> |