Compare commits

...

5 Commits

Author SHA1 Message Date
chrome-storm-c442
289ce65431 chore: remove 12 old exe from git + use git rm in cleanup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 09:32:01 -05:00
chrome-storm-c442
704ce3bef2 v1.9.12: cleanup orphan Gitea tags alongside old releases
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 09:29:53 -05:00
chrome-storm-c442
00f3b76d2a v1.9.11: always check updates on startup + cleanup Gitea releases
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 09:27:21 -05:00
chrome-storm-c442
efbbfa13ee v1.9.10: cleanup old Gitea releases — keep first + last 5
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 09:21:33 -05:00
chrome-storm-c442
3c4d02c5ec v1.9.9: fix stale server data after Edit — force reconnect with fresh credentials
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 09:11:36 -05:00
17 changed files with 123 additions and 7 deletions

View File

@@ -182,8 +182,9 @@ def build():
# Auto-deploy: sync shared files so Claude Code always has the latest
deploy_shared_files()
# Publish release to Gitea
# Publish release to Gitea + cleanup old remote releases
publish_gitea_release(dst)
cleanup_gitea_releases()
def _get_gitea_auth() -> dict:
@@ -311,6 +312,78 @@ def _version_key(path: str):
return (0, 0, 0)
def _tag_version_key(tag_name: str):
"""Extract (major, minor, patch) from tag like 'v1.9.5'."""
m = re.match(r'v(\d+)\.(\d+)\.(\d+)', tag_name)
if m:
return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
return (0, 0, 0)
def cleanup_gitea_releases():
"""Keep the first release (v1.0.0) and the last 5 on Gitea, delete the rest + orphan tags."""
auth = _get_gitea_auth()
if not auth:
return
# --- Clean releases ---
try:
req = urllib.request.Request(f"{_GITEA_API}/releases?limit=50", headers=auth)
resp = urllib.request.urlopen(req, timeout=30)
releases = json.loads(resp.read())
except Exception as e:
print(f"Gitea release list failed: {e}")
return
keep_tags = set()
if len(releases) > 6:
releases.sort(key=lambda r: _tag_version_key(r.get("tag_name", "")))
first = releases[0]
last_5 = releases[-5:]
keep_ids = {first["id"]} | {r["id"] for r in last_5}
keep_tags = {first.get("tag_name")} | {r.get("tag_name") for r in last_5}
removed = []
for r in releases:
if r["id"] in keep_ids:
continue
try:
req = urllib.request.Request(
f"{_GITEA_API}/releases/{r['id']}", headers=auth, method="DELETE")
urllib.request.urlopen(req, timeout=15)
removed.append(r.get("tag_name", "?"))
except Exception as e:
print(f"Failed to delete Gitea release {r.get('tag_name')}: {e}")
if removed:
print(f"Cleaned {len(removed)} old Gitea releases: {', '.join(removed)}")
else:
keep_tags = {r.get("tag_name") for r in releases}
# --- Clean orphan tags (tags without releases) ---
try:
req = urllib.request.Request(f"{_GITEA_API}/tags?limit=50", headers=auth)
resp = urllib.request.urlopen(req, timeout=30)
tags = json.loads(resp.read())
except Exception:
return
removed_tags = []
for tag in tags:
name = tag.get("name", "")
if name in keep_tags:
continue
try:
req = urllib.request.Request(
f"{_GITEA_API}/tags/{name}", headers=auth, method="DELETE")
urllib.request.urlopen(req, timeout=10)
removed_tags.append(name)
except Exception:
pass
if removed_tags:
print(f"Cleaned {len(removed_tags)} orphan Gitea tags: {', '.join(removed_tags)}")
def cleanup_old_releases():
"""Keep the first release (v1.0.0) and the last 5 releases, delete the rest."""
import glob
@@ -327,9 +400,20 @@ def cleanup_old_releases():
keep = set([first] + last_5)
removed = []
_flags = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
for f in all_exes:
if f not in keep:
os.remove(f)
# Use git rm so deletion is staged for commit
try:
subprocess.run(
["git", "rm", "-f", "--quiet", f],
cwd=PROJECT_DIR, creationflags=_flags,
capture_output=True,
)
except Exception:
# Fallback: just delete the file
if os.path.exists(f):
os.remove(f)
removed.append(os.path.basename(f))
if removed:

View File

@@ -9,6 +9,15 @@ from typing import Dict, Optional, Tuple
from core.ssh_client import ShellSession, SFTPSession
_CRITICAL_KEYS = ('ip', 'port', 'username', 'password', 'type',
'access_key', 'secret_key', 'use_ssl')
def _server_changed(old: dict, new: dict) -> bool:
"""Check if critical connection fields differ."""
return any(old.get(k) != new.get(k) for k in _CRITICAL_KEYS)
class SessionData:
"""Container for session data including the actual sessions and their metadata."""
def __init__(self, alias: str, server: dict, key_path: str):
@@ -70,6 +79,11 @@ class SessionPool:
self._sessions[alias] = session_data
else:
session_data = self._sessions[alias]
# Invalidate if server connection data changed
if _server_changed(session_data.server, server):
session_data.cleanup()
session_data.server = server
session_data.key_path = key_path
# Update access time for LRU
self._update_last_access(alias)
@@ -108,6 +122,11 @@ class SessionPool:
self._sessions[alias] = session_data
else:
session_data = self._sessions[alias]
# Invalidate if server connection data changed
if _server_changed(session_data.server, server):
session_data.cleanup()
session_data.server = server
session_data.key_path = key_path
# Update access time for LRU
self._update_last_access(alias)

View File

@@ -401,12 +401,15 @@ del /f /q "%~f0" >nul 2>&1
return
time.sleep(0.1)
first_run = True
while self._running:
# Check if enough time passed since last check
# On first run after startup, always check regardless of interval
last_check = self.store.get_last_update_check()
now = time.time()
if not last_check or (now - last_check) >= _CHECK_INTERVAL:
if first_run or not last_check or (now - last_check) >= _CHECK_INTERVAL:
first_run = False
info = self.check_now()
if info and self._gui_callback:
mode = self.store.get_update_mode()

View File

@@ -299,9 +299,19 @@ class App(ctk.CTk):
self.sidebar._select(new_alias)
self.session_pool.rename_server(alias, new_alias)
else:
info = self._tab_instances.get("info")
if info and hasattr(info, "refresh"):
info.refresh()
# Data may have changed (IP, port, password) — force reconnect
self._force_reconnect(alias)
def _force_reconnect(self, alias: str):
"""Force tabs to reconnect after server data changed."""
# Invalidate cached SSH/SFTP sessions in pool
self.session_pool.disconnect_session(alias)
# Reset _current_alias so set_server() bypasses early return
for widget in self._tab_instances.values():
if getattr(widget, '_current_alias', None) == alias:
widget._current_alias = None
# Re-trigger server selection (calls set_server on all tabs)
self._on_server_select(alias)
def _delete_server(self, alias: str):
if messagebox.askyesno(t("delete_server"), t("delete_confirm").format(alias=alias)):

View File

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