v1.2.0 + v1.3.0: Localization, About dialog, TOTP/2FA, stability improvements

v1.2.0:
- GUI localization (EN/RU/ZH) with language switcher and persistent selection
- About dialog (ⓘ) with app info, features, quick start guide
- core/i18n.py — internationalization module with t() function
- All GUI components translated via t() keys

v1.3.0:
- TOTP/2FA tab — Google Authenticator compatible codes with live 30s countdown,
  one-click copy, per-server secret management
- core/totp.py — TOTP module (pyotp, RFC 6238)
- core/logger.py — rotating file logger (5MB, 3 backups)
- Stronger Fernet encryption key with automatic migration from old key
- Thread-safe server store with locks, atomic writes, auto-restore on corruption
- Parallel status checks via ThreadPoolExecutor (up to 10 concurrent)
- SSH client: explicit channel cleanup, Unix key permissions
- Server dialog: port validation (1-65535), TOTP secret field
- Language change preserves active tab and server selection
- pyotp dependency added

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chrome-storm-c442
2026-02-23 11:07:51 -05:00
parent f86d6a7214
commit bf39fd7b67
26 changed files with 2029 additions and 246 deletions

View File

@@ -1,10 +1,14 @@
"""
Setup tab — one-click installation for Claude Code integration.
Includes configuration path management and backup/restore.
"""
import os
import threading
from tkinter import filedialog, messagebox
import customtkinter as ctk
from core.claude_setup import check_status, install_all, install_ssh_script, install_skill, generate_ssh_key
from core.i18n import t
class SetupTab(ctk.CTkFrame):
@@ -13,50 +17,54 @@ class SetupTab(ctk.CTkFrame):
self.store = store
# Header
ctk.CTkLabel(
self, text="Claude Code Integration",
self.header_label = ctk.CTkLabel(
self, text=t("claude_integration"),
font=ctk.CTkFont(size=20, weight="bold")
).pack(padx=20, pady=(20, 5))
)
self.header_label.pack(padx=20, pady=(20, 5))
ctk.CTkLabel(
self,
text="Setup everything so Claude Code can manage your servers via /ssh skill.\n"
"Both GUI and Claude Code share the same servers.json — add a server here,\n"
"Claude sees it immediately.",
self.desc_label = ctk.CTkLabel(
self, text=t("claude_desc"),
text_color="#9ca3af", justify="center"
).pack(padx=20, pady=(0, 15))
)
self.desc_label.pack(padx=20, pady=(0, 15))
# Status card
self.status_frame = ctk.CTkFrame(self)
self.status_frame.pack(fill="x", padx=20, pady=10)
ctk.CTkLabel(
self.status_frame, text="Status",
self.status_title = ctk.CTkLabel(
self.status_frame, text=t("status"),
font=ctk.CTkFont(size=14, weight="bold"), anchor="w"
).pack(fill="x", padx=15, pady=(10, 5))
)
self.status_title.pack(fill="x", padx=15, pady=(10, 5))
self._status_labels: dict[str, ctk.CTkLabel] = {}
self._status_text_labels: dict[str, ctk.CTkLabel] = {}
status_items = [
("shared_dir", "Shared config dir (~/.server-connections)"),
("servers_json", "servers.json"),
("ssh_script", "ssh.py (CLI tool)"),
("skill_installed", "/ssh skill for Claude Code"),
("ssh_key_exists", "SSH key (ed25519)"),
("shared_dir", "status_shared_dir"),
("servers_json", "status_servers_json"),
("ssh_script", "status_ssh_script"),
("encryption", "status_encryption"),
("skill_installed", "status_skill"),
("ssh_key_exists", "status_ssh_key"),
]
for key, label in status_items:
for key, i18n_key in status_items:
row = ctk.CTkFrame(self.status_frame, fg_color="transparent")
row.pack(fill="x", padx=15, pady=2)
indicator = ctk.CTkLabel(row, text="\u25cf", width=20, text_color="#6b7280")
indicator.pack(side="left")
ctk.CTkLabel(row, text=label, anchor="w").pack(side="left", fill="x", expand=True)
text_label = ctk.CTkLabel(row, text=t(i18n_key), anchor="w")
text_label.pack(side="left", fill="x", expand=True)
self._status_labels[key] = indicator
self._status_text_labels[key] = (text_label, i18n_key)
# Buttons
btn_frame = ctk.CTkFrame(self, fg_color="transparent")
btn_frame.pack(fill="x", padx=20, pady=15)
self.install_all_btn = ctk.CTkButton(
btn_frame, text="Install Everything",
btn_frame, text=t("install_everything"),
font=ctk.CTkFont(size=14, weight="bold"),
height=40, fg_color="#22c55e", hover_color="#16a34a",
command=self._install_all
@@ -67,14 +75,71 @@ class SetupTab(ctk.CTkFrame):
ind_frame = ctk.CTkFrame(btn_frame, fg_color="transparent")
ind_frame.pack(fill="x")
ctk.CTkButton(ind_frame, text="ssh.py", width=100, fg_color="#6b7280",
command=self._install_script).pack(side="left", padx=(0, 5))
ctk.CTkButton(ind_frame, text="/ssh skill", width=100, fg_color="#6b7280",
command=self._install_skill).pack(side="left", padx=5)
ctk.CTkButton(ind_frame, text="SSH key", width=100, fg_color="#6b7280",
command=self._gen_key).pack(side="left", padx=5)
ctk.CTkButton(ind_frame, text="Refresh", width=80, fg_color="#3b82f6",
command=self._refresh_status).pack(side="right")
self.ssh_py_btn = ctk.CTkButton(ind_frame, text=t("install_ssh_py"), width=100, fg_color="#6b7280",
command=self._install_script)
self.ssh_py_btn.pack(side="left", padx=(0, 5))
self.skill_btn = ctk.CTkButton(ind_frame, text=t("install_skill"), width=100, fg_color="#6b7280",
command=self._install_skill)
self.skill_btn.pack(side="left", padx=5)
self.ssh_key_btn = ctk.CTkButton(ind_frame, text=t("install_ssh_key"), width=100, fg_color="#6b7280",
command=self._gen_key)
self.ssh_key_btn.pack(side="left", padx=5)
self.refresh_btn = ctk.CTkButton(ind_frame, text=t("refresh"), width=80, fg_color="#3b82f6",
command=self._refresh_status)
self.refresh_btn.pack(side="right")
# ── Configuration section ─────────────────────
config_frame = ctk.CTkFrame(self)
config_frame.pack(fill="x", padx=20, pady=(5, 5))
self.config_title = ctk.CTkLabel(
config_frame, text=t("configuration"),
font=ctk.CTkFont(size=14, weight="bold"), anchor="w"
)
self.config_title.pack(fill="x", padx=15, pady=(10, 5))
# Config path row
path_row = ctk.CTkFrame(config_frame, fg_color="transparent")
path_row.pack(fill="x", padx=15, pady=5)
self.config_label = ctk.CTkLabel(path_row, text=t("config_label"), anchor="w", width=60)
self.config_label.pack(side="left")
self._path_label = ctk.CTkLabel(
path_row, text=store.get_config_path(),
anchor="w", text_color="#9ca3af",
font=ctk.CTkFont(family="Consolas", size=11)
)
self._path_label.pack(side="left", fill="x", expand=True, padx=(5, 10))
self.change_path_btn = ctk.CTkButton(
path_row, text=t("change_path"), width=100, fg_color="#6b7280",
command=self._change_config_path
)
self.change_path_btn.pack(side="right")
# Backup row
backup_row = ctk.CTkFrame(config_frame, fg_color="transparent")
backup_row.pack(fill="x", padx=15, pady=(5, 10))
self.backup_btn = ctk.CTkButton(
backup_row, text=t("backup_now"), width=100, fg_color="#3b82f6",
command=self._backup_now
)
self.backup_btn.pack(side="left", padx=(0, 10))
self._backup_var = ctk.StringVar(value=t("select_backup"))
backups = store.list_backups()
values = backups if backups else [t("no_backups")]
self._backup_menu = ctk.CTkOptionMenu(
backup_row, variable=self._backup_var,
values=values, width=250
)
self._backup_menu.pack(side="left", padx=(0, 10))
self.restore_btn = ctk.CTkButton(
backup_row, text=t("restore"), width=80, fg_color="#ef4444", hover_color="#dc2626",
command=self._restore_backup
)
self.restore_btn.pack(side="left")
# Log
self.log = ctk.CTkTextbox(self, height=150, font=ctk.CTkFont(family="Consolas", size=11), state="disabled")
@@ -98,15 +163,15 @@ class SetupTab(ctk.CTkFrame):
label.configure(text="\u25cf", text_color="#ef4444") # red
def _install_all(self):
self.install_all_btn.configure(state="disabled", text="Installing...")
self.install_all_btn.configure(state="disabled", text=t("installing_all"))
def _do():
results = install_all()
for msg in results:
self.after(0, lambda m=msg: self._log(m))
self.after(0, self._refresh_status)
self.after(0, lambda: self._log("\nDone! Claude Code can now use /ssh to manage your servers."))
self.after(0, lambda: self.install_all_btn.configure(state="normal", text="Install Everything"))
self.after(0, lambda: self._log("\n" + t("install_done")))
self.after(0, lambda: self.install_all_btn.configure(state="normal", text=t("install_everything")))
threading.Thread(target=_do, daemon=True).start()
@@ -124,3 +189,46 @@ class SetupTab(ctk.CTkFrame):
msg = generate_ssh_key()
self._log(msg)
self._refresh_status()
# ── Configuration methods ─────────────────────────
def _change_config_path(self):
path = filedialog.askopenfilename(
title=t("select_servers_json"),
filetypes=[("JSON files", "*.json"), ("All files", "*.*")],
initialdir=os.path.dirname(self.store.get_config_path())
)
if path:
self.store.set_config_path(path)
self._path_label.configure(text=path)
self._log(t("config_changed").format(path=path))
def _backup_now(self):
try:
name = self.store.create_backup()
self._log(t("backup_created").format(name=name))
self._refresh_backups()
except Exception as e:
self._log(t("backup_failed").format(e=e))
def _restore_backup(self):
selected = self._backup_var.get()
if not selected or selected in (t("select_backup"), t("no_backups")):
self._log(t("no_backup_selected"))
return
if not messagebox.askyesno(t("restore_backup_title"), t("restore_confirm").format(name=selected)):
return
try:
self.store.restore_backup(selected)
self._log(t("restored").format(name=selected))
except Exception as e:
self._log(t("restore_failed").format(e=e))
def _refresh_backups(self):
backups = self.store.list_backups()
if backups:
self._backup_menu.configure(values=backups)
self._backup_var.set(backups[0])
else:
self._backup_menu.configure(values=[t("no_backups")])
self._backup_var.set(t("no_backups"))