main
7 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ef059647fa |
Reduire la taille du formulaire de session (connexion rapide SFTP/SSH)
Signale par l'utilisateur : le dialogue de "SFTP sur cette connexion" ne
tenait pas sur l'ecran. Deux causes distinctes, verifiees par capture d'ecran
avant de conclure (rendu hors-ecran + grab()) :
1. Bug reel, dans les deux dialogues qui ont une section Bastion (SessionDialog
et GroupSettingsDialog) : leur QStackedWidget reservait par defaut la
hauteur de sa PLUS GRANDE page (Configuration manuelle, 5 champs) meme
quand la page vide "Aucun" etait affichee - environ 200px de vide pur,
visible sur la premiere capture. Nouveau app/ui/auto_height_stack.py
(AutoHeightStackedWidget) : le sizeHint suit la page COURANTE, pas la plus
grande de toutes: -186px des l'ouverture, a bastion desactive.
2. Choix d'ergonomie : une connexion rapide sert a aller vite (souvent un
serveur de test en DHCP, cf. le README) - Bastion/Keepalive/Reconnexion y
sont maintenant repliees par defaut derriere un bouton "Options
avancees...", toujours accessibles mais qui n'allongent plus le dialogue
pour le cas courant. Se deplient automatiquement si "Heriter du groupe"
ramene un bastion herite, pour ne pas cacher un changement silencieux.
Le dialogue complet ("Nouvelle session" depuis le panneau lateral) est
inchange : tout reste visible d'emblee, c'est une operation reflechie, pas
une connexion a la volee.
Resultat mesure : dialogue de connexion rapide 1026px -> 492px replie (-52%),
876px si depliees (au lieu de 1026, meme contenu, sans le vide). Le dialogue
"Nouvelle session" complet passe de meme a 816px grace au seul correctif (1).
tests/test_session_dialog_compact.py (5 verifications) : sizeHint de
AutoHeightStackedWidget sur la page courante, repli par defaut en connexion
rapide, redimensionnement au depli, absence de repli hors connexion rapide,
depli automatique sur heritage de groupe. Suite complete revalidee (35/35),
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>
|
||
|
|
9216f636bd |
feat: ajouter le Credential Vault (memorisation securisee optionnelle)
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>
|
||
|
|
b615ed52af |
feat: ajouter les tags de session et les parametres par defaut de groupe
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> |
||
|
|
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> |
||
|
|
b977842186 |
Client SSH/SFTP personnel pour Windows
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> |