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>
Par defaut, inchange : les mots de passe sont toujours redemandes a chaque
connexion. Nouvelle case a cocher optionnelle dans l'editeur de session,
visible seulement en authentification par mot de passe : "Memoriser le mot
de passe de maniere securisee".
- app/credentials.py : abstraction CredentialStore (set_secret/get_secret/
delete_secret/is_available), pour supporter plus tard d'autres systemes que
Windows. WindowsCredentialStore utilise Windows Credential Manager
(CredWrite/CredRead/CredDelete) via pywin32-ctypes - deja present dans
l'environnement (dependance de PyInstaller), mais pas encore declare comme
dependance d'execution : ajoute a requirements.txt (marque sys_platform ==
"win32"), sinon la fonctionnalite aurait silencieusement ete indisponible
sur une installation propre. NullCredentialStore en repli si indisponible :
jamais d'ecriture en clair a la place.
- Session.password_storage ("none" par defaut | "secure") : seulement le mode
est stocke dans sessions.json, jamais le secret. Le mot de passe est ecrit/
lu dans le trousseau via une cle applicative (session:<id>), et supprime
automatiquement si l'utilisateur desactive la memorisation ou supprime la
session.
- Un bastion en mode "session PySSH" reutilise un mot de passe deja memorise
pour cette session (lecture seule depuis ce chemin : l'ecriture ne se fait
que via l'ouverture directe de la session concernee).
tests/test_credentials.py : NullCredentialStore, aller-retour reel avec
Windows Credential Manager (nettoye et verifie apres coup, aucune entree
residuelle), integration ConnectionController (memorise puis reutilise sans
redemander), suppression en cascade, reutilisation par un bastion, et
verification que le secret n'apparait jamais dans la serialisation JSON de
la session.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Tags : une session peut porter plusieurs tags libres (Session.tags), saisis
comme une liste separee par des virgules dans l'editeur (normalises : espaces
retires, doublons insensibles a la casse elimines), avec autocompletion sur
les tags deja utilises. Affiches dans l'arbre des sessions, et filtrables via
un champ dedie au-dessus de l'arbre (masque les sessions ne correspondant pas,
sans toucher aux dossiers/terminaux locaux/hosts OpenSSH).
Parametres de groupe : choix architectural deliberement plus simple qu'une
resolution dynamique a la connexion. app/groups.py (GroupSettingsStore, meme
pattern que les autres stores) associe un dossier (Session.group) a des
valeurs par defaut (utilisateur, port, authentification, cle, bastion,
keepalive). Ces valeurs ne s'appliquent qu'au moment de la creation d'une
session, via un bouton explicite "↓ Heriter du groupe" dans SessionDialog -
jamais de resolution automatique/retroactive. Ce choix evite d'introduire des
valeurs sentinelles "herite vs. explicite" sur chaque champ (port=22 est a la
fois une valeur par defaut ET une vraie valeur possible, par exemple) dans
tout le reste du code (ConnectionController, SSHConnection...), pour une
fonctionnalite que la priorisation du projet place parmi les secondaires. Une
session reste ensuite un ensemble de valeurs normales, librement modifiables
- "surcharger une valeur heritee" est donc trivialement satisfait.
Clic droit sur un dossier dans le panneau lateral : "Nouvelle session dans ce
dossier..." (pre-remplit le champ Dossier) et "Parametres du groupe..."
(GroupSettingsDialog).
tests/test_tags_and_groups.py : persistance des tags, CRUD GroupSettingsStore,
normalisation de la saisie des tags, heritage applique/absent, filtrage dans
l'arbre.
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>
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>
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>