Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
259caacb01 | ||
|
|
6c5ceead09 | ||
|
|
65c1f809b1 | ||
|
|
064de8df8d | ||
|
|
7522908404 | ||
|
|
c21b263b24 |
12
core/i18n.py
12
core/i18n.py
@@ -111,6 +111,10 @@ _EN = {
|
||||
"term_connecting": "Connecting to {alias}...",
|
||||
"term_connected": "Connected to {alias}",
|
||||
"term_disconnected": "Disconnected",
|
||||
"term_off": "OFF",
|
||||
"ctx_disconnect": "Disconnect",
|
||||
"term_click_to_connect": "Double-click to connect to {alias}",
|
||||
"sftp_click_to_connect": "Double-click server to browse files",
|
||||
"term_reconnecting": "Reconnecting ({n}/{max})...",
|
||||
"term_connect_failed": "Connection failed: {error}",
|
||||
"term_reconnect_fail": "Disconnected (reconnect failed)",
|
||||
@@ -634,6 +638,10 @@ _RU = {
|
||||
"term_connecting": "Подключение к {alias}...",
|
||||
"term_connected": "Подключено к {alias}",
|
||||
"term_disconnected": "Отключено",
|
||||
"term_off": "ОТКЛ",
|
||||
"ctx_disconnect": "Отключиться",
|
||||
"term_click_to_connect": "Двойной клик для подключения к {alias}",
|
||||
"sftp_click_to_connect": "Двойной клик для просмотра файлов",
|
||||
"term_reconnecting": "Переподключение ({n}/{max})...",
|
||||
"term_connect_failed": "Ошибка подключения: {error}",
|
||||
"term_reconnect_fail": "Отключено (не удалось переподключиться)",
|
||||
@@ -1157,6 +1165,10 @@ _ZH = {
|
||||
"term_connecting": "正在连接 {alias}...",
|
||||
"term_connected": "已连接到 {alias}",
|
||||
"term_disconnected": "已断开",
|
||||
"term_off": "已断开",
|
||||
"ctx_disconnect": "断开连接",
|
||||
"term_click_to_connect": "双击连接 {alias}",
|
||||
"sftp_click_to_connect": "双击服务器浏览文件",
|
||||
"term_reconnecting": "重新连接中 ({n}/{max})...",
|
||||
"term_connect_failed": "连接失败:{error}",
|
||||
"term_reconnect_fail": "已断开(重连失败)",
|
||||
|
||||
@@ -210,6 +210,7 @@ CTX_ICONS = {
|
||||
"ctx_open_browser": "browser",
|
||||
"ctx_check_status": "status_check",
|
||||
"ctx_copy_alias": "copy",
|
||||
"ctx_disconnect": "close",
|
||||
"edit": "edit",
|
||||
"delete": "delete",
|
||||
}
|
||||
|
||||
@@ -255,4 +255,14 @@ class SessionPool:
|
||||
)
|
||||
if has_active:
|
||||
active.append(alias)
|
||||
return active
|
||||
return active
|
||||
|
||||
def has_active_session(self, alias: str) -> bool:
|
||||
with self._lock:
|
||||
sd = self._sessions.get(alias)
|
||||
if not sd:
|
||||
return False
|
||||
return bool(
|
||||
(sd.shell_session and sd.shell_session.connected) or
|
||||
(sd.sftp_session and sd.sftp_session.connected)
|
||||
)
|
||||
26
gui/app.py
26
gui/app.py
@@ -154,7 +154,7 @@ class App(ctk.CTk):
|
||||
self._paned.pack(fill="both", expand=True)
|
||||
|
||||
# Sidebar
|
||||
self.sidebar = Sidebar(self._paned, self.store, on_select=self._on_server_select, session_pool=self.session_pool)
|
||||
self.sidebar = Sidebar(self._paned, self.store, on_select=self._on_server_select, on_double_click=self._on_server_connect, session_pool=self.session_pool)
|
||||
self._paned.add(self.sidebar, minsize=180, width=self.store._sidebar_width)
|
||||
self.sidebar.add_callback = self._add_server
|
||||
self.sidebar.edit_callback = self._edit_server
|
||||
@@ -163,6 +163,7 @@ class App(ctk.CTk):
|
||||
self.sidebar.open_tab_callback = self._context_open_tab
|
||||
self.sidebar.check_status_callback = self._context_check_status
|
||||
self.sidebar.open_browser_callback = self._context_open_browser
|
||||
self.sidebar.disconnect_callback = self._on_server_disconnect
|
||||
|
||||
# Main area
|
||||
self._main_frame = ctk.CTkFrame(self._paned, fg_color="transparent")
|
||||
@@ -264,6 +265,11 @@ class App(ctk.CTk):
|
||||
widget.pack(fill="both", expand=True)
|
||||
self._tab_instances[key] = widget
|
||||
|
||||
# Wire disconnect callback for terminal toolbar button
|
||||
terminal = self._tab_instances.get("terminal")
|
||||
if terminal and hasattr(terminal, "_on_disconnect_callback"):
|
||||
terminal._on_disconnect_callback = self._on_server_disconnect
|
||||
|
||||
# Restore previously active tab if still available
|
||||
if restore_tab_key and restore_tab_key in self._tab_keys:
|
||||
try:
|
||||
@@ -307,6 +313,20 @@ class App(ctk.CTk):
|
||||
# Update session indicators after a short delay (connection is async)
|
||||
self.after(1500, self.sidebar.update_session_indicators)
|
||||
|
||||
def _on_server_connect(self, alias: str):
|
||||
"""Double-click: connect interactive tabs (terminal, files, powershell)."""
|
||||
for key, widget in self._tab_instances.items():
|
||||
if hasattr(widget, "connect"):
|
||||
widget.connect()
|
||||
|
||||
def _on_server_disconnect(self, alias: str):
|
||||
"""Disconnect all sessions for a server."""
|
||||
for key, widget in self._tab_instances.items():
|
||||
if hasattr(widget, "disconnect"):
|
||||
widget.disconnect()
|
||||
self.session_pool.disconnect_session(alias)
|
||||
self.after(500, self.sidebar.update_session_indicators)
|
||||
|
||||
def _add_server(self):
|
||||
dialog = ServerDialog(self, self.store)
|
||||
self.wait_window(dialog)
|
||||
@@ -356,6 +376,10 @@ class App(ctk.CTk):
|
||||
self.tabview.set(_tab_label(tab_key))
|
||||
except Exception:
|
||||
pass
|
||||
# Connect the target tab if it supports explicit connection
|
||||
widget = self._tab_instances.get(tab_key)
|
||||
if widget and hasattr(widget, "connect"):
|
||||
widget.connect()
|
||||
|
||||
def _context_check_status(self, alias: str):
|
||||
"""Context menu: check single server status in background."""
|
||||
|
||||
@@ -34,10 +34,11 @@ _CONTEXT_ACTIONS = {
|
||||
|
||||
|
||||
class Sidebar(ctk.CTkFrame):
|
||||
def __init__(self, master, store, on_select=None, session_pool=None):
|
||||
def __init__(self, master, store, on_select=None, on_double_click=None, session_pool=None):
|
||||
super().__init__(master, width=250, corner_radius=0)
|
||||
self.store = store
|
||||
self.on_select = on_select
|
||||
self.on_double_click = on_double_click
|
||||
self.session_pool = session_pool
|
||||
self._selected_alias: str | None = None
|
||||
self._server_frames: dict[str, ctk.CTkFrame] = {}
|
||||
@@ -96,6 +97,7 @@ class Sidebar(ctk.CTkFrame):
|
||||
self.open_tab_callback = None # (alias, tab_key) → select server + switch tab
|
||||
self.check_status_callback = None # (alias) → check single server
|
||||
self.open_browser_callback = None # (alias) → open server URL in browser
|
||||
self.disconnect_callback = None # (alias) → disconnect all sessions
|
||||
|
||||
# Subscribe to store changes
|
||||
self.store.subscribe(self._refresh_list)
|
||||
@@ -272,6 +274,7 @@ class Sidebar(ctk.CTkFrame):
|
||||
# Click handlers
|
||||
for widget in [frame, info, name_label, detail_label, badge, type_badge, session_ind]:
|
||||
widget.bind("<Button-1>", lambda e, a=alias: self._select(a))
|
||||
widget.bind("<Double-Button-1>", lambda e, a=alias: self._on_double_click(a))
|
||||
widget.bind("<Button-3>", lambda e, a=alias: self._show_context_menu(e, a))
|
||||
|
||||
self._server_frames[alias] = frame
|
||||
@@ -371,6 +374,11 @@ class Sidebar(ctk.CTkFrame):
|
||||
if self.on_select:
|
||||
self.on_select(alias)
|
||||
|
||||
def _on_double_click(self, alias: str):
|
||||
self._select(alias)
|
||||
if self.on_double_click:
|
||||
self.on_double_click(alias)
|
||||
|
||||
def _highlight_selected(self):
|
||||
for alias, frame in self._server_frames.items():
|
||||
if alias == self._selected_alias:
|
||||
@@ -460,6 +468,18 @@ class Sidebar(ctk.CTkFrame):
|
||||
if actions:
|
||||
menu.add_separator()
|
||||
|
||||
# Dynamic disconnect if session is active
|
||||
if self.session_pool and self.session_pool.has_active_session(alias):
|
||||
dc_icon = icon(CTX_ICONS.get("ctx_disconnect", ""))
|
||||
dc_label = f"{dc_icon} {t('ctx_disconnect')}" if dc_icon else t("ctx_disconnect")
|
||||
menu.add_command(
|
||||
label=dc_label,
|
||||
command=lambda a=alias: (
|
||||
self.disconnect_callback(a) if self.disconnect_callback else None
|
||||
),
|
||||
)
|
||||
menu.add_separator()
|
||||
|
||||
# "Move to Group" submenu
|
||||
groups = self.store.get_groups()
|
||||
if groups:
|
||||
|
||||
@@ -307,13 +307,21 @@ class FilesTab(ctk.CTkFrame):
|
||||
stored_path, stored_sudo = self.session_pool.get_sftp_state(alias)
|
||||
if stored_path != "/":
|
||||
self._remote_path = stored_path
|
||||
# The stored sudo mode will be applied when the connection is established
|
||||
self._connect_sftp()
|
||||
self._remote_status.configure(text=t("sftp_click_to_connect"))
|
||||
else:
|
||||
self._remote_list.populate([])
|
||||
self._remote_status.configure(text=t("connect_to_browse"))
|
||||
self._set_remote_buttons_state("disabled")
|
||||
|
||||
def connect(self):
|
||||
"""Explicitly connect SFTP (double-click or context menu)."""
|
||||
if self._current_alias and not self._sftp:
|
||||
self._connect_sftp()
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnect SFTP and update UI (called by app)."""
|
||||
self._disconnect_sftp()
|
||||
|
||||
# ── SFTP connection ──
|
||||
|
||||
def _connect_sftp(self):
|
||||
|
||||
@@ -97,7 +97,18 @@ class PowershellTab(ctk.CTkFrame):
|
||||
self._set_status(t("ps_disconnected"), "#888888")
|
||||
return
|
||||
|
||||
self._connect(alias)
|
||||
self._set_status(t("term_click_to_connect").format(alias=alias), "#f59e0b")
|
||||
|
||||
def connect(self):
|
||||
"""Explicitly connect WinRM (double-click or context menu)."""
|
||||
if self._current_alias and not self._client:
|
||||
self._connect(self._current_alias)
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnect WinRM and update UI (called by app)."""
|
||||
self._disconnect()
|
||||
if self._current_alias:
|
||||
self._set_status(t("term_click_to_connect").format(alias=self._current_alias), "#f59e0b")
|
||||
|
||||
# ── Connection ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -29,6 +29,18 @@ class TerminalTab(ctk.CTkFrame):
|
||||
# Import here to avoid circular issues
|
||||
from gui.widgets.terminal_widget import TerminalWidget
|
||||
|
||||
self._toolbar = ctk.CTkFrame(self, fg_color="transparent", height=32)
|
||||
self._toolbar.pack(fill="x", padx=5, pady=(5, 0))
|
||||
self._toolbar.pack_propagate(False)
|
||||
self._conn_btn = ctk.CTkButton(
|
||||
self._toolbar, text=t("ctx_connect"), width=120, height=28,
|
||||
fg_color="#6b7280", hover_color="#4b5563",
|
||||
font=ctk.CTkFont(size=12), state="disabled",
|
||||
command=self._on_conn_btn_click,
|
||||
)
|
||||
self._conn_btn.pack(side="right", padx=2)
|
||||
self._connected = False
|
||||
|
||||
self._terminal = TerminalWidget(
|
||||
self,
|
||||
send_callback=self._send_to_shell,
|
||||
@@ -37,6 +49,15 @@ class TerminalTab(ctk.CTkFrame):
|
||||
on_font_size_changed=self._on_font_size_changed,
|
||||
)
|
||||
self._terminal.pack(fill="both", expand=True, padx=5, pady=5)
|
||||
|
||||
# Overlay "OFF" label (shown when disconnected)
|
||||
self._overlay = ctk.CTkLabel(
|
||||
self._terminal, text=t("term_off"),
|
||||
font=ctk.CTkFont(size=72, weight="bold"),
|
||||
text_color=("#cccccc", "#333333"),
|
||||
fg_color="transparent",
|
||||
)
|
||||
self._overlay.place(relx=0.5, rely=0.45, anchor="center")
|
||||
self._terminal.set_status(t("term_disconnected"), "#888888")
|
||||
|
||||
# Thread-safe data queue
|
||||
@@ -45,6 +66,7 @@ class TerminalTab(ctk.CTkFrame):
|
||||
# Sudo auto-password detection
|
||||
self._sudo_buffer = b"" # Buffer for detecting sudo prompts
|
||||
self._sudo_sent = False # Prevent sending password twice for same prompt
|
||||
self._on_disconnect_callback = None
|
||||
|
||||
def set_server(self, alias: str | None):
|
||||
if alias == self._current_alias:
|
||||
@@ -61,11 +83,48 @@ class TerminalTab(ctk.CTkFrame):
|
||||
|
||||
self._current_alias = alias
|
||||
if alias:
|
||||
self._connect()
|
||||
self._set_conn_btn_disconnected()
|
||||
self._conn_btn.configure(state="normal")
|
||||
self._terminal.set_status(t("term_click_to_connect").format(alias=alias), "#f59e0b")
|
||||
else:
|
||||
self._set_conn_btn_disconnected()
|
||||
self._conn_btn.configure(state="disabled")
|
||||
self._terminal.reset()
|
||||
self._terminal.set_status(t("term_disconnected"), "#888888")
|
||||
|
||||
def connect(self):
|
||||
"""Explicitly connect (double-click or context menu)."""
|
||||
if self._current_alias and not self._session:
|
||||
self._connect()
|
||||
|
||||
def _on_conn_btn_click(self):
|
||||
if self._connected:
|
||||
if self._on_disconnect_callback and self._current_alias:
|
||||
self._on_disconnect_callback(self._current_alias)
|
||||
else:
|
||||
self.connect()
|
||||
|
||||
def _set_conn_btn_connected(self):
|
||||
self._connected = True
|
||||
self._conn_btn.configure(
|
||||
text=t("ctx_disconnect"), fg_color="#dc2626", hover_color="#b91c1c", state="normal",
|
||||
)
|
||||
self._overlay.place_forget()
|
||||
|
||||
def _set_conn_btn_disconnected(self):
|
||||
self._connected = False
|
||||
self._conn_btn.configure(
|
||||
text=t("ctx_connect"), fg_color="#6b7280", hover_color="#4b5563",
|
||||
)
|
||||
self._overlay.place(relx=0.5, rely=0.45, anchor="center")
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnect and update UI (called by app)."""
|
||||
self._disconnect()
|
||||
self._set_conn_btn_disconnected()
|
||||
if self._current_alias:
|
||||
self._terminal.set_status(t("term_click_to_connect").format(alias=self._current_alias), "#f59e0b")
|
||||
|
||||
def _connect(self):
|
||||
if not self._current_alias:
|
||||
return
|
||||
@@ -135,6 +194,7 @@ class TerminalTab(ctk.CTkFrame):
|
||||
# Only grab focus if terminal tab is currently visible
|
||||
if self._terminal.winfo_ismapped():
|
||||
self._terminal.focus_terminal()
|
||||
self._set_conn_btn_connected()
|
||||
self.after(0, _set_session)
|
||||
except Exception as e:
|
||||
self.after(0, lambda: self._terminal.set_status(
|
||||
|
||||
160
plans/disable-terminal-autoconnect.md
Normal file
160
plans/disable-terminal-autoconnect.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# Отключить автоподключение терминала при одинарном клике
|
||||
|
||||
## Контекст
|
||||
|
||||
При одинарном клике на сервер в sidebar все табы (terminal, files, powershell) сразу подключаются к серверу. Пользователь хочет просто переключаться между серверами без автоподключения. Подключение — только по двойному клику.
|
||||
|
||||
## Подход
|
||||
|
||||
- **Одинарный клик** — выбрать сервер, обновить табы (info, setup, keys и т.д.), но НЕ подключаться к terminal/files/powershell
|
||||
- **Двойной клик** — выбрать сервер + подключить все "connecting" табы (terminal, files, powershell)
|
||||
- **Контекстное меню** "Open Terminal" / "Browse Files" — тоже подключает
|
||||
|
||||
Tkinter при двойном клике генерирует оба события: `<Button-1>` (первый клик) → `<Double-Button-1>`. Это нам на руку: первый клик выберет сервер, двойной клик — подключит. Debounce не нужен.
|
||||
|
||||
## Изменения — 4 файла
|
||||
|
||||
### 1. `gui/sidebar.py` — добавить двойной клик
|
||||
|
||||
**Строка 37** — добавить `on_double_click` в конструктор:
|
||||
```python
|
||||
def __init__(self, master, store, on_select=None, on_double_click=None, session_pool=None):
|
||||
...
|
||||
self.on_select = on_select
|
||||
self.on_double_click = on_double_click
|
||||
```
|
||||
|
||||
**Строки 272-275** — добавить `<Double-Button-1>` binding:
|
||||
```python
|
||||
for widget in [frame, info, name_label, detail_label, badge, type_badge, session_ind]:
|
||||
widget.bind("<Button-1>", lambda e, a=alias: self._select(a))
|
||||
widget.bind("<Double-Button-1>", lambda e, a=alias: self._on_double_click(a))
|
||||
widget.bind("<Button-3>", lambda e, a=alias: self._show_context_menu(e, a))
|
||||
```
|
||||
|
||||
**После `_select()`** (строка 372) — новый метод:
|
||||
```python
|
||||
def _on_double_click(self, alias: str):
|
||||
self._select(alias)
|
||||
if self.on_double_click:
|
||||
self.on_double_click(alias)
|
||||
```
|
||||
|
||||
### 2. `gui/tabs/terminal_tab.py` — убрать автоподключение
|
||||
|
||||
**Строки 49-67** — `set_server()`: заменить `self._connect()` на показ статуса:
|
||||
```python
|
||||
def set_server(self, alias: str | None):
|
||||
if alias == self._current_alias:
|
||||
return
|
||||
if self._current_alias and self._session and self.session_pool:
|
||||
buf = self._terminal.get_current_buffer()
|
||||
self.session_pool.store_shell_state(self._current_alias, buf)
|
||||
self._disconnect()
|
||||
self._current_alias = alias
|
||||
if alias:
|
||||
self._terminal.set_status(t("term_click_to_connect").format(alias=alias), "#f59e0b")
|
||||
else:
|
||||
self._terminal.reset()
|
||||
self._terminal.set_status(t("term_disconnected"), "#888888")
|
||||
```
|
||||
|
||||
**Добавить публичный метод `connect()`** после `set_server()`:
|
||||
```python
|
||||
def connect(self):
|
||||
"""Explicitly connect (double-click or context menu)."""
|
||||
if self._current_alias and not self._session:
|
||||
self._connect()
|
||||
```
|
||||
|
||||
### 3. `gui/tabs/files_tab.py` — убрать автоподключение
|
||||
|
||||
**Строки 304-311** — `set_server()`: заменить `self._connect_sftp()` на статус:
|
||||
```python
|
||||
if alias:
|
||||
if self.session_pool:
|
||||
stored_path, stored_sudo = self.session_pool.get_sftp_state(alias)
|
||||
if stored_path != "/":
|
||||
self._remote_path = stored_path
|
||||
self._remote_status.configure(text=t("sftp_click_to_connect"))
|
||||
else:
|
||||
...
|
||||
```
|
||||
|
||||
**Добавить публичный метод `connect()`**:
|
||||
```python
|
||||
def connect(self):
|
||||
"""Explicitly connect SFTP (double-click or context menu)."""
|
||||
if self._current_alias and not self._sftp:
|
||||
self._connect_sftp()
|
||||
```
|
||||
|
||||
### 4. `gui/tabs/powershell_tab.py` — убрать автоподключение
|
||||
|
||||
**Строка 100** — заменить `self._connect(alias)` на статус:
|
||||
```python
|
||||
if alias is None:
|
||||
self._set_status(t("ps_disconnected"), "#888888")
|
||||
return
|
||||
self._set_status(t("term_click_to_connect").format(alias=alias), "#f59e0b")
|
||||
```
|
||||
|
||||
**Добавить публичный метод `connect()`**:
|
||||
```python
|
||||
def connect(self):
|
||||
"""Explicitly connect WinRM (double-click or context menu)."""
|
||||
if self._current_alias and not self._client:
|
||||
self._connect(self._current_alias)
|
||||
```
|
||||
|
||||
### 5. `gui/app.py` — подключить двойной клик
|
||||
|
||||
**Строка 157** — передать `on_double_click`:
|
||||
```python
|
||||
self.sidebar = Sidebar(self._paned, self.store,
|
||||
on_select=self._on_server_select,
|
||||
on_double_click=self._on_server_connect,
|
||||
session_pool=self.session_pool)
|
||||
```
|
||||
|
||||
**Новый метод `_on_server_connect()`** (после `_on_server_select`):
|
||||
```python
|
||||
def _on_server_connect(self, alias: str):
|
||||
"""Double-click: connect interactive tabs (terminal, files, powershell)."""
|
||||
for key, widget in self._tab_instances.items():
|
||||
if hasattr(widget, "connect"):
|
||||
widget.connect()
|
||||
```
|
||||
|
||||
**Строки 350-358** — `_context_open_tab()`: добавить вызов `connect()`:
|
||||
```python
|
||||
def _context_open_tab(self, alias: str, tab_key: str):
|
||||
self._on_server_select(alias)
|
||||
self.sidebar._select(alias)
|
||||
if tab_key in self._tab_keys:
|
||||
try:
|
||||
self.tabview.set(_tab_label(tab_key))
|
||||
except Exception:
|
||||
pass
|
||||
# Connect the target tab if it supports explicit connection
|
||||
widget = self._tab_instances.get(tab_key)
|
||||
if widget and hasattr(widget, "connect"):
|
||||
widget.connect()
|
||||
```
|
||||
|
||||
### 6. `core/i18n.py` — 2 ключа перевода
|
||||
|
||||
Рядом с `term_disconnected`:
|
||||
|
||||
| Ключ | EN | RU | ZH |
|
||||
|------|----|----|-----|
|
||||
| `term_click_to_connect` | `Double-click to connect to {alias}` | `Двойной клик для подключения к {alias}` | `双击连接 {alias}` |
|
||||
| `sftp_click_to_connect` | `Double-click server to browse files` | `Двойной клик для просмотра файлов` | `双击服务器浏览文件` |
|
||||
|
||||
## Верификация
|
||||
|
||||
1. `python build.py` — собрать exe
|
||||
2. Запустить exe, одинарный клик на SSH-сервер → терминал показывает "Двойной клик для подключения", файлы показывают аналогичное сообщение, info таб работает как раньше
|
||||
3. Двойной клик на сервер → терминал и файлы подключаются
|
||||
4. Правый клик → "Open Terminal" → терминал подключается
|
||||
5. Переключение между серверами одним кликом → нет автоподключений, быстрое переключение
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
releases/ServerManager-v1.9.29-win-x64.exe
Normal file
BIN
releases/ServerManager-v1.9.29-win-x64.exe
Normal file
Binary file not shown.
Binary file not shown.
@@ -64,7 +64,7 @@ python ~/.server-connections/ssh.py --sql-databases "mariadb-alias"
|
||||
|
||||
## Общие команды
|
||||
|
||||
### Список серверов (безопасный — alias, тип, ключ, заметки)
|
||||
### Список серверов (безопасный — alias, тип, группа, ключ, заметки)
|
||||
```bash
|
||||
python ~/.server-connections/ssh.py --list
|
||||
```
|
||||
|
||||
21
tools/ssh.py
21
tools/ssh.py
@@ -102,6 +102,11 @@ def load_servers():
|
||||
return data, {s["alias"]: s for s in data.get("servers", [])}
|
||||
|
||||
|
||||
def _group_map(data: dict) -> dict:
|
||||
"""Map group UUID → group name."""
|
||||
return {g["id"]: g.get("name", "") for g in data.get("groups", [])}
|
||||
|
||||
|
||||
def save_servers(data):
|
||||
servers_file = _get_servers_file()
|
||||
text = json.dumps(data, indent=2, ensure_ascii=False)
|
||||
@@ -779,7 +784,8 @@ def ping_server(server: dict):
|
||||
|
||||
|
||||
def list_servers(full=False):
|
||||
_, servers = load_servers()
|
||||
data, servers = load_servers()
|
||||
groups = _group_map(data)
|
||||
if full:
|
||||
# WARNING: full mode shows sensitive data (IP, port, user)
|
||||
# Only for local/manual use, NEVER through AI API
|
||||
@@ -791,13 +797,14 @@ def list_servers(full=False):
|
||||
print(f"{alias:<20} {s['ip']:<20} {s.get('port', 22):<8} {s.get('user', 'root'):<10} {has_key:<6}")
|
||||
else:
|
||||
# Safe mode: only aliases (no IPs, ports, users)
|
||||
print(f"{'Alias':<20} {'Type':<10} {'Key':<6} {'Notes'}")
|
||||
print("-" * 70)
|
||||
print(f"{'Alias':<20} {'Type':<10} {'Group':<14} {'Key':<6} {'Notes'}")
|
||||
print("-" * 80)
|
||||
for alias, s in servers.items():
|
||||
has_key = "yes" if os.path.exists(SSH_KEY_PATH) else "no"
|
||||
stype = s.get("type", "ssh")
|
||||
group_name = groups.get(s.get("group", ""), "-")
|
||||
notes = s.get("notes", "")
|
||||
print(f"{alias:<20} {stype:<10} {has_key:<6} {notes}")
|
||||
print(f"{alias:<20} {stype:<10} {group_name:<14} {has_key:<6} {notes}")
|
||||
|
||||
|
||||
def _resolve_alias(alias: str, servers: dict) -> str:
|
||||
@@ -831,12 +838,16 @@ def _resolve_alias(alias: str, servers: dict) -> str:
|
||||
|
||||
def server_info(alias: str):
|
||||
"""Show server info safe for AI context — NO ip, user, password, port, totp_secret."""
|
||||
_, servers = load_servers()
|
||||
data, servers = load_servers()
|
||||
groups = _group_map(data)
|
||||
alias = _resolve_alias(alias, servers)
|
||||
s = servers[alias]
|
||||
has_key = "yes" if os.path.exists(SSH_KEY_PATH) else "no"
|
||||
print(f"Alias: {s['alias']}")
|
||||
print(f"Type: {s.get('type', 'ssh')}")
|
||||
group_name = groups.get(s.get("group", ""), "")
|
||||
if group_name:
|
||||
print(f"Group: {group_name}")
|
||||
print(f"Key: {has_key}")
|
||||
print(f"Auth: {s.get('auth', 'password')}")
|
||||
print(f"2FA: {'yes' if s.get('totp_secret') else 'no'}")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Version info for ServerManager."""
|
||||
|
||||
__version__ = "1.9.24"
|
||||
__version__ = "1.9.30"
|
||||
__app_name__ = "ServerManager"
|
||||
__author__ = "aibot777"
|
||||
__description__ = "Desktop GUI for managing remote servers"
|
||||
|
||||
Reference in New Issue
Block a user