Add startup connection validation to GUI

- Add validate_token() method to RegistryClient
  - Returns (is_valid, error_message) tuple
  - Catches 401/UNAUTHORIZED errors and connection issues
- Add startup validation in MainWindow
  - Runs 500ms after window shows (non-blocking)
  - Clears invalid tokens automatically
  - Shows status bar message explaining what happened
- Document cmdforge config disconnect command

This prevents confusing errors when trying to publish with stale or
revoked credentials - the GUI now auto-clears bad connections on restart.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rob 2026-01-17 01:59:16 -04:00
parent 66fd121ee4
commit 85d1212c0d
3 changed files with 62 additions and 1 deletions

View File

@ -31,6 +31,13 @@ All notable changes to CmdForge will be documented in this file.
- Auto-fetch registry version when opening publish dialog - Auto-fetch registry version when opening publish dialog
- Fork detection during publish workflow - Fork detection during publish workflow
- Always refresh tools page after publish dialog closes - Always refresh tools page after publish dialog closes
- **Startup connection validation**: GUI validates registry token on startup
- Automatically clears invalid/revoked tokens
- Shows status bar message when connection is cleared
- Prevents confusing errors when trying to publish with stale credentials
#### CLI Features
- `cmdforge config disconnect` - Clear registry token from local configuration
#### Admin Features #### Admin Features
- Maintenance section in admin dashboard - Maintenance section in admin dashboard

View File

@ -5,7 +5,7 @@ from PySide6.QtWidgets import (
QListWidget, QListWidgetItem, QStackedWidget, QListWidget, QListWidgetItem, QStackedWidget,
QStatusBar, QLabel, QSplitter QStatusBar, QLabel, QSplitter
) )
from PySide6.QtCore import Qt, QSize, QSettings from PySide6.QtCore import Qt, QSize, QSettings, QTimer
from PySide6.QtGui import QIcon, QFont, QShortcut, QKeySequence from PySide6.QtGui import QIcon, QFont, QShortcut, QKeySequence
from .styles import STYLESHEET from .styles import STYLESHEET
@ -68,6 +68,33 @@ class MainWindow(QMainWindow):
# Restore window geometry # Restore window geometry
self._restore_geometry() self._restore_geometry()
# Validate registry connection after window shows
QTimer.singleShot(500, self._validate_registry_connection)
def _validate_registry_connection(self):
"""Check if registry connection is valid on startup, clear if invalid."""
try:
from ..registry_client import RegistryClient
from ..config import set_registry_token, load_config
config = load_config()
if not config.registry.token:
return # No connection configured, nothing to validate
client = RegistryClient()
is_valid, error = client.validate_token()
if not is_valid:
# Token is invalid, clear it
set_registry_token(None)
self.status_bar.showMessage(
f"Registry connection cleared: {error}. Please reconnect.",
10000 # Show for 10 seconds
)
except Exception as e:
# Don't crash on validation errors, just log to status
self.status_bar.showMessage(f"Could not validate registry connection: {e}", 5000)
def _setup_sidebar(self): def _setup_sidebar(self):
"""Set up sidebar navigation items.""" """Set up sidebar navigation items."""
items = [ items = [

View File

@ -605,6 +605,33 @@ class RegistryClient:
return response.json().get("data", {}) return response.json().get("data", {})
def validate_token(self) -> tuple[bool, str]:
"""
Validate that the current token is valid.
Returns:
Tuple of (is_valid, error_message)
- (True, "") if token is valid
- (False, "reason") if token is invalid or missing
"""
if not self.token:
return False, "No token configured"
try:
response = self._request("GET", "/me/tools", require_auth=True)
if response.status_code == 200:
return True, ""
elif response.status_code == 401:
return False, "Token is invalid or revoked"
else:
return False, f"Unexpected response: {response.status_code}"
except RegistryError as e:
if e.http_status == 401 or e.code == "UNAUTHORIZED":
return False, "Token is invalid or revoked"
return False, str(e.message)
except Exception as e:
return False, f"Connection error: {e}"
def get_my_tools(self) -> List[ToolInfo]: def get_my_tools(self) -> List[ToolInfo]:
""" """
Get tools published by the authenticated user. Get tools published by the authenticated user.