Compare commits

..

4 Commits

Author SHA1 Message Date
chrome-storm-c442
35bdefba59 v1.9.19: fix offscreen window — validate saved geometry, reject -32000 coords
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 04:13:11 -05:00
chrome-storm-c442
d33f573483 v1.9.18: revert GUI to v1.9.14 state — fix broken window display
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 04:09:47 -05:00
chrome-storm-c442
cf319c502e v1.9.16: add --s3-url presigned URL command to ssh.py
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 04:05:00 -05:00
chrome-storm-c442
01ab318e4b v1.9.15: fix minimize/restore — remove grab_set, add Win32 restore fallback
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 03:58:32 -05:00
10 changed files with 48 additions and 67 deletions

View File

@@ -91,7 +91,7 @@ class App(ctk.CTk):
# Restore saved window geometry or use default
saved_geo = self.store._window_geometry
if saved_geo:
if saved_geo and self._is_valid_geometry(saved_geo):
self.geometry(saved_geo)
else:
self.geometry("1100x700")
@@ -667,10 +667,25 @@ class App(ctk.CTk):
except Exception:
pass
@staticmethod
def _is_valid_geometry(geo: str) -> bool:
"""Reject geometry with offscreen coordinates (e.g. minimized -32000)."""
try:
# format: WxH+X+Y or WxH-X-Y
import re
m = re.match(r"(\d+)x(\d+)([+-]\d+)([+-]\d+)", geo)
if not m:
return False
x, y = int(m.group(3)), int(m.group(4))
return -100 < x < 10000 and -100 < y < 10000
except Exception:
return False
def _on_close(self):
# Save window geometry (size + position) and sidebar width
try:
self.store._window_geometry = self.geometry()
geo = self.geometry()
self.store._window_geometry = geo if self._is_valid_geometry(geo) else None
# Save sidebar width from PanedWindow sash position
try:
sash_pos = self._paned.sash_coord(0)

View File

@@ -33,12 +33,6 @@ class GroupDialog(ctk.CTkToplevel):
self.resizable(False, False)
self.transient(master)
self.grab_set()
self.protocol("WM_DELETE_WINDOW", self._on_close)
# Fix: restore dialog when parent is un-minimized
self._master_ref = master
master.bind("<Map>", self._on_parent_map, add="+")
self.bind("<Unmap>", self._on_unmap, add="+")
# ── Name ──
ctk.CTkLabel(self, text=t("group_name"), anchor="w").pack(
@@ -77,7 +71,7 @@ class GroupDialog(ctk.CTkToplevel):
btn_frame.pack(fill="x", padx=20, pady=(15, 10))
ctk.CTkButton(btn_frame, text=t("cancel"), width=80,
fg_color="gray", command=self._on_close).pack(side="left")
fg_color="gray", command=self.destroy).pack(side="left")
ctk.CTkButton(btn_frame, text=t("save"), width=80,
command=self._save).pack(side="right")
@@ -96,34 +90,6 @@ class GroupDialog(ctk.CTkToplevel):
else:
btn.configure(border_color=fg)
def _on_parent_map(self, event=None):
try:
if not self.winfo_exists():
return
self.deiconify()
self.lift()
self.focus_force()
self.grab_set()
except Exception:
pass
def _on_unmap(self, event=None):
try:
self.grab_release()
except Exception:
pass
def _on_close(self):
try:
self._master_ref.unbind("<Map>")
except Exception:
pass
try:
self.grab_release()
except Exception:
pass
self.destroy()
def _save(self):
name = self._name_var.get().strip()
if not name:
@@ -141,4 +107,4 @@ class GroupDialog(ctk.CTkToplevel):
group = self.store.add_group(name, self._selected_color)
self.result = group
self._on_close()
self.destroy()

View File

@@ -62,11 +62,6 @@ class ServerDialog(ctk.CTkToplevel):
# Release grab on close (prevents stuck app)
self.protocol("WM_DELETE_WINDOW", self._on_close)
# Fix: restore dialog when parent is un-minimized
self._master_ref = master
master.bind("<Map>", self._on_parent_map, add="+")
self.bind("<Unmap>", self._on_unmap, add="+")
self._field_frames: dict[str, ctk.CTkFrame] = {}
self._build_ui(server)
@@ -490,31 +485,8 @@ class ServerDialog(ctk.CTkToplevel):
except ValueError as e:
self._show_error(str(e))
def _on_parent_map(self, event=None):
"""Force-restore dialog when parent window is un-minimized."""
try:
if not self.winfo_exists():
return
self.deiconify()
self.lift()
self.focus_force()
self.grab_set()
except Exception:
pass
def _on_unmap(self, event=None):
"""Release grab when dialog is minimized to prevent input lock."""
try:
self.grab_release()
except Exception:
pass
def _on_close(self):
"""Release grab and destroy — prevents stuck app on minimize."""
try:
self._master_ref.unbind("<Map>")
except Exception:
pass
try:
self.grab_release()
except Exception:

View File

@@ -42,6 +42,7 @@ S3 (type: s3):
python ssh.py --s3-upload ALIAS local bucket/key # upload file
python ssh.py --s3-download ALIAS bucket/key local # download file
python ssh.py --s3-delete ALIAS bucket/key # delete object
python ssh.py --s3-url ALIAS bucket/key [SEC] # presigned URL (default 3600s)
WinRM (type: winrm):
python ssh.py --ps ALIAS "Get-Process" # PowerShell via WinRM
@@ -1459,6 +1460,27 @@ def s3_delete(server: dict, remote_path: str):
sys.exit(1)
def s3_url(server: dict, remote_path: str, expires: int = 3600):
"""Generate a presigned URL for an S3 object."""
client = _get_s3_client(server)
parts = remote_path.split("/", 1)
bucket = parts[0] if parts else server.get("bucket", "")
key = parts[1] if len(parts) > 1 else ""
if not bucket or not key:
print("ERROR: Usage: --s3-url ALIAS bucket/key [seconds]", file=sys.stderr)
sys.exit(1)
try:
url = client.generate_presigned_url(
"get_object",
Params={"Bucket": bucket, "Key": key},
ExpiresIn=expires,
)
print(url)
except Exception as e:
print(f"ERROR: {e}", file=sys.stderr)
sys.exit(1)
# ── Grafana commands ──────────────────────────────────
def _grafana_request(server: dict, endpoint: str) -> dict:
@@ -1763,6 +1785,12 @@ def main():
alias = _resolve_alias(sys.argv[2], servers)
s3_delete(servers[alias], sys.argv[3])
sys.exit(0)
if cmd == "--s3-url" and len(sys.argv) >= 4:
_, servers = load_servers()
alias = _resolve_alias(sys.argv[2], servers)
expires = int(sys.argv[4]) if len(sys.argv) >= 5 else 3600
s3_url(servers[alias], sys.argv[3], expires)
sys.exit(0)
# ── Grafana commands ──
if cmd == "--grafana-dashboards" and len(sys.argv) >= 3:

View File

@@ -1,6 +1,6 @@
"""Version info for ServerManager."""
__version__ = "1.9.14"
__version__ = "1.9.19"
__app_name__ = "ServerManager"
__author__ = "aibot777"
__description__ = "Desktop GUI for managing remote servers"