feat: multi-type server support — SQL, Redis, Grafana, Prometheus, Telnet, WinRM, RDP/VNC

Full implementation of multi-type server management across GUI and CLI:

New clients: SQLClient (MariaDB/MSSQL/PostgreSQL), RedisClient, GrafanaClient,
PrometheusClient, TelnetSession, WinRMClient, RemoteDesktopLauncher.

New GUI tabs: QueryTab (SQL editor + Treeview), RedisTab (console + history),
GrafanaTab (dashboards + alerts), PrometheusTab (PromQL + targets),
PowershellTab (PS/CMD), LaunchTab (RDP/VNC external client).

Infrastructure: TAB_REGISTRY for conditional tabs per server type,
adaptive server_dialog fields, colored type badges in sidebar,
status checker for all types (SSH/TCP/SQL/Redis/HTTP), 100+ i18n keys.

CLI: ssh.py extended with --sql, --redis, --grafana-*, --prom-*, --ps, --cmd.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chrome-storm-c442
2026-02-24 09:35:24 -05:00
parent 2d1d942ddc
commit eede67e6a9
26 changed files with 3990 additions and 168 deletions

View File

@@ -20,6 +20,43 @@ from gui.tabs.info_tab import InfoTab
from gui.tabs.keys_tab import KeysTab
from gui.tabs.setup_tab import SetupTab
from gui.tabs.totp_tab import TOTPTab
from gui.tabs.query_tab import QueryTab
from gui.tabs.redis_tab import RedisTab
from gui.tabs.grafana_tab import GrafanaTab
from gui.tabs.prometheus_tab import PrometheusTab
from gui.tabs.powershell_tab import PowershellTab
from gui.tabs.launch_tab import LaunchTab
# Tab sets per server type — determines which tabs are shown
TAB_REGISTRY = {
"ssh": ["terminal", "files", "info", "keys", "totp", "setup"],
"telnet": ["terminal", "info", "setup"],
"winrm": ["powershell", "info", "setup"],
"mariadb": ["query", "info", "setup"],
"mssql": ["query", "info", "setup"],
"postgresql": ["query", "info", "setup"],
"redis": ["console", "info", "setup"],
"grafana": ["dashboards", "info", "setup"],
"prometheus": ["metrics", "info", "setup"],
"rdp": ["launch", "info", "setup"],
"vnc": ["launch", "info", "setup"],
}
# Map tab key → widget class (used as lazy factory)
TAB_CLASSES = {
"terminal": TerminalTab,
"files": FilesTab,
"info": InfoTab,
"keys": KeysTab,
"totp": TOTPTab,
"setup": SetupTab,
"query": QueryTab,
"console": RedisTab,
"dashboards": GrafanaTab,
"metrics": PrometheusTab,
"powershell": PowershellTab,
"launch": LaunchTab,
}
class App(ctk.CTk):
@@ -67,11 +104,11 @@ class App(ctk.CTk):
self.sidebar.delete_callback = self._delete_server
# Main area
main = ctk.CTkFrame(self, fg_color="transparent")
main.pack(side="right", fill="both", expand=True)
self._main_frame = ctk.CTkFrame(self, fg_color="transparent")
self._main_frame.pack(side="right", fill="both", expand=True)
# Header bar (language + about)
header_bar = ctk.CTkFrame(main, fg_color="transparent", height=40)
header_bar = ctk.CTkFrame(self._main_frame, fg_color="transparent", height=40)
header_bar.pack(fill="x", padx=10, pady=(8, 0))
header_bar.pack_propagate(False)
@@ -93,39 +130,96 @@ class App(ctk.CTk):
)
self.about_btn.pack(side="right", padx=(5, 5))
# Tabview
self.tabview = ctk.CTkTabview(main, command=self._on_tab_changed)
# Initialize tab tracking
self.tabview = None
self._tab_keys = []
self._tab_instances = {}
# Build default SSH tab set
self._rebuild_tabs(TAB_REGISTRY["ssh"])
def _rebuild_tabs(self, tab_keys: list[str], restore_tab_key: str | None = None):
"""Destroy current tabview and rebuild with the given tab keys."""
# Remember current active tab
if restore_tab_key is None:
restore_tab_key = self._get_current_tab_key() if self._tab_keys else None
# Destroy old tab instances
for key, widget in self._tab_instances.items():
try:
widget.pack_forget()
widget.destroy()
except Exception:
pass
self._tab_instances = {}
# Destroy old tabview
if self.tabview is not None:
try:
self.tabview.destroy()
except Exception:
pass
# Store new tab key list
self._tab_keys = list(tab_keys)
# Create new tabview
self.tabview = ctk.CTkTabview(self._main_frame, command=self._on_tab_changed)
self.tabview.pack(fill="both", expand=True, padx=10, pady=10)
# Tab names stored for language updates
self._tab_keys = ["terminal", "files", "info", "keys", "totp", "setup"]
for key in self._tab_keys:
self.tabview.add(t(key))
self.terminal_tab = TerminalTab(self.tabview.tab(t("terminal")), self.store, self.session_pool)
self.terminal_tab.pack(fill="both", expand=True)
# Create tab instances using TAB_CLASSES factory
for key in self._tab_keys:
cls = TAB_CLASSES.get(key)
if cls is None:
continue
parent = self.tabview.tab(t(key))
widget = self._create_tab_instance(cls, key, parent)
widget.pack(fill="both", expand=True)
self._tab_instances[key] = widget
self.files_tab = FilesTab(self.tabview.tab(t("files")), self.store, self.session_pool)
self.files_tab.pack(fill="both", expand=True)
# Restore previously active tab if still available
if restore_tab_key and restore_tab_key in self._tab_keys:
try:
self.tabview.set(t(restore_tab_key))
except Exception:
pass
self.info_tab = InfoTab(self.tabview.tab(t("info")), self.store, edit_callback=self._edit_server)
self.info_tab.pack(fill="both", expand=True)
self.keys_tab = KeysTab(self.tabview.tab(t("keys")), self.store)
self.keys_tab.pack(fill="both", expand=True)
self.totp_tab = TOTPTab(self.tabview.tab(t("totp")), self.store)
self.totp_tab.pack(fill="both", expand=True)
self.setup_tab = SetupTab(self.tabview.tab(t("setup")), self.store)
self.setup_tab.pack(fill="both", expand=True)
def _create_tab_instance(self, cls, key: str, parent):
"""Create a tab widget instance with the correct constructor args."""
if cls in (TerminalTab, FilesTab):
return cls(parent, self.store, self.session_pool)
elif cls is InfoTab:
return cls(parent, self.store, edit_callback=self._edit_server)
elif cls is SetupTab:
return cls(parent, self.store)
elif cls in (KeysTab, TOTPTab):
return cls(parent, self.store)
else:
# QueryTab, RedisTab, GrafanaTab, PrometheusTab, PowershellTab, LaunchTab
return cls(parent, self.store)
def _on_server_select(self, alias: str):
self.terminal_tab.set_server(alias)
self.files_tab.set_server(alias)
self.info_tab.set_server(alias)
self.keys_tab.set_server(alias)
self.totp_tab.set_server(alias)
# Determine server type and required tabs
if alias:
server = self.store.get_server(alias)
server_type = server.get("type", "ssh") if server else "ssh"
else:
server_type = "ssh"
new_tab_keys = TAB_REGISTRY.get(server_type, TAB_REGISTRY["ssh"])
# Rebuild tabs only if the tab set changed
if new_tab_keys != self._tab_keys:
self._rebuild_tabs(new_tab_keys)
# Notify each tab instance about the selected server
for key, widget in self._tab_instances.items():
if hasattr(widget, "set_server"):
widget.set_server(alias)
# Update session indicators after a short delay (connection is async)
self.after(1500, self.sidebar.update_session_indicators)
@@ -143,7 +237,9 @@ class App(ctk.CTk):
self.sidebar._select(new_alias)
self.session_pool.rename_server(alias, new_alias)
else:
self.info_tab.refresh()
info = self._tab_instances.get("info")
if info and hasattr(info, "refresh"):
info.refresh()
def _delete_server(self, alias: str):
if messagebox.askyesno(t("delete_server"), t("delete_confirm").format(alias=alias)):
@@ -155,7 +251,9 @@ class App(ctk.CTk):
def _on_status_update(self):
self.sidebar.update_statuses()
self.sidebar.update_session_indicators()
self.info_tab.refresh()
info = self._tab_instances.get("info")
if info and hasattr(info, "refresh"):
info.refresh()
def _show_about(self):
AboutDialog(self)
@@ -190,76 +288,42 @@ class App(ctk.CTk):
# Remember selected server
alias = self.sidebar.get_selected()
# Use provided key or default to first tab
current_key = restore_tab_key or self._tab_keys[0]
current_key = restore_tab_key or (self._tab_keys[0] if self._tab_keys else "terminal")
# Save state before destroying tabs
saved_remote_path = self.files_tab._remote_path
saved_local_path = self.files_tab._local_path
had_sftp = self.files_tab._sftp is not None and self.files_tab._sftp.connected
# Save FilesTab state if it exists
files_tab = self._tab_instances.get("files")
saved_remote_path = None
saved_local_path = None
had_sftp = False
if files_tab:
saved_remote_path = files_tab._remote_path
saved_local_path = files_tab._local_path
had_sftp = files_tab._sftp is not None and files_tab._sftp.connected
# Disconnect all sessions in the pool
self.session_pool.disconnect_all()
# Detach tab contents
self.terminal_tab.pack_forget()
self.files_tab.pack_forget()
self.info_tab.pack_forget()
self.keys_tab.pack_forget()
self.totp_tab.pack_forget()
self.setup_tab.pack_forget()
# Rebuild tabs with translated names (same tab keys, just new language)
self._rebuild_tabs(self._tab_keys, restore_tab_key=current_key)
# Get the main frame and destroy old tabview
main = self.tabview.master
self.tabview.destroy()
# Restore FilesTab state if it exists in new tab set
files_tab = self._tab_instances.get("files")
if files_tab:
files_tab._local_path = saved_local_path
files_tab._refresh_local()
if alias and had_sftp:
files_tab._remote_path = saved_remote_path
files_tab.set_server(alias)
elif alias:
files_tab.set_server(alias)
# Create new tabview with translated names
self.tabview = ctk.CTkTabview(main, command=self._on_tab_changed)
self.tabview.pack(fill="both", expand=True, padx=10, pady=10)
for key in self._tab_keys:
self.tabview.add(t(key))
# Re-parent tab contents
self.terminal_tab = TerminalTab(self.tabview.tab(t("terminal")), self.store, self.session_pool)
self.terminal_tab.pack(fill="both", expand=True)
self.files_tab = FilesTab(self.tabview.tab(t("files")), self.store, self.session_pool)
self.files_tab.pack(fill="both", expand=True)
self.info_tab = InfoTab(self.tabview.tab(t("info")), self.store, edit_callback=self._edit_server)
self.info_tab.pack(fill="both", expand=True)
self.keys_tab = KeysTab(self.tabview.tab(t("keys")), self.store)
self.keys_tab.pack(fill="both", expand=True)
self.totp_tab = TOTPTab(self.tabview.tab(t("totp")), self.store)
self.totp_tab.pack(fill="both", expand=True)
self.setup_tab = SetupTab(self.tabview.tab(t("setup")), self.store)
self.setup_tab.pack(fill="both", expand=True)
# Restore active tab by key
try:
self.tabview.set(t(current_key))
except Exception:
pass
# Restore file paths and reconnect properly
self.files_tab._local_path = saved_local_path
self.files_tab._refresh_local()
if alias and had_sftp:
# Had active SFTP — reconnect and restore remote path
self.files_tab._remote_path = saved_remote_path
self.files_tab.set_server(alias)
elif alias:
self.files_tab.set_server(alias)
# Restore server selection for other tabs (terminal auto-reconnects)
# Restore server selection for all other tabs
if alias:
self.terminal_tab.set_server(alias)
self.info_tab.set_server(alias)
self.keys_tab.set_server(alias)
self.totp_tab.set_server(alias)
for key, widget in self._tab_instances.items():
if key == "files":
continue # Already handled above
if hasattr(widget, "set_server"):
widget.set_server(alias)
# Update sidebar
self.sidebar.update_language()
@@ -367,14 +431,22 @@ class App(ctk.CTk):
"""Handle tab switch — manage terminal focus."""
try:
current = self.tabview.get()
if current == t("terminal"):
self.terminal_tab._terminal.focus_terminal()
terminal = self._tab_instances.get("terminal")
if terminal and current == t("terminal"):
terminal._terminal.focus_terminal()
else:
self.focus_set()
except Exception:
pass
def _on_close(self):
# Clean up tab instances
for key, widget in self._tab_instances.items():
if hasattr(widget, "on_close"):
try:
widget.on_close()
except Exception:
pass
# Disconnect all sessions before closing
self.session_pool.disconnect_all()
self.checker.stop()