Add fork display, version selector, and admin cleanup features

Registry Features:
- Fork tracking with forked_from/forked_version metadata
- Forked tools show "Forked from" notice on detail page
- Original tools display "Forks" section listing all forks
- Fork count in tool stats
- API: GET /api/v1/tools/<owner>/<name>/forks

GUI Improvements:
- Version selector dropdown for registry installs
- Fetch available versions via GET /api/v1/tools/<owner>/<name>/versions
- "Latest" option plus all available versions listed

Admin Features:
- POST /api/v1/admin/cleanup/rejected endpoint
- Maintenance section in admin dashboard
- "Dry Run" and "Run Cleanup" buttons for rejected version cleanup
- Configurable grace period (default 7 days)

Documentation:
- Updated CHANGELOG.md with all recent changes

Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
rob
2026-01-16 23:42:32 -04:00
co-authored by Claude Opus 4.5
parent 74b5124360
commit adefe909c2
6 changed files with 494 additions and 11 deletions
+92 -9
View File
@@ -57,9 +57,9 @@ class SearchWorker(QThread):
self.error.emit(str(e))
class InstallWorker(QThread):
"""Background worker for tool installation."""
finished = Signal(str)
class VersionsWorker(QThread):
"""Background worker to fetch available versions."""
finished = Signal(list) # List of version strings
error = Signal(str)
def __init__(self, owner: str, name: str):
@@ -70,8 +70,29 @@ class InstallWorker(QThread):
def run(self):
try:
client = RegistryClient()
client.install_tool(self.owner, self.name)
self.finished.emit(f"{self.owner}/{self.name}")
versions = client.get_tool_versions(self.owner, self.name)
self.finished.emit(versions)
except Exception as e:
self.error.emit(str(e))
class InstallWorker(QThread):
"""Background worker for tool installation."""
finished = Signal(str)
error = Signal(str)
def __init__(self, owner: str, name: str, version: str = None):
super().__init__()
self.owner = owner
self.name = name
self.version = version
def run(self):
try:
client = RegistryClient()
client.install_tool(self.owner, self.name, version=self.version)
version_str = f"@{self.version}" if self.version else ""
self.finished.emit(f"{self.owner}/{self.name}{version_str}")
except Exception as e:
self.error.emit(str(e))
@@ -84,7 +105,9 @@ class RegistryPage(QWidget):
self.main_window = main_window
self._search_worker = None
self._install_worker = None
self._versions_worker = None
self._selected_tool = None
self._available_versions = []
self._current_page = 1
self._total_pages = 1
self._current_tags = []
@@ -228,6 +251,23 @@ class RegistryPage(QWidget):
right_layout.addWidget(details_box, 1)
# Version selector row
version_row = QHBoxLayout()
version_row.setSpacing(8)
version_label = QLabel("Version:")
version_label.setStyleSheet("color: #4a5568;")
version_row.addWidget(version_label)
self.version_combo = QComboBox()
self.version_combo.setMinimumWidth(120)
self.version_combo.addItem("Latest", None)
self.version_combo.setEnabled(False)
version_row.addWidget(self.version_combo)
version_row.addStretch()
right_layout.addLayout(version_row)
# Action buttons
actions = QHBoxLayout()
@@ -405,6 +445,9 @@ class RegistryPage(QWidget):
self.details_text.clear()
self.btn_install.setEnabled(False)
self.btn_update.hide()
self.version_combo.clear()
self.version_combo.addItem("Latest", None)
self.version_combo.setEnabled(False)
return
row = items[0].row()
@@ -413,11 +456,47 @@ class RegistryPage(QWidget):
self._selected_tool = tool
self._show_tool_details(tool)
# Reset and fetch versions
self.version_combo.clear()
self.version_combo.addItem("Loading...", None)
self.version_combo.setEnabled(False)
self._fetch_versions(tool.get("owner", ""), tool.get("name", ""))
# Check if installed / update available
tool_name = tool.get("name", "")
self._update_install_buttons()
def _fetch_versions(self, owner: str, name: str):
"""Fetch available versions for a tool."""
self._versions_worker = VersionsWorker(owner, name)
self._versions_worker.finished.connect(self._on_versions_fetched)
self._versions_worker.error.connect(self._on_versions_error)
self._versions_worker.start()
def _on_versions_fetched(self, versions: list):
"""Handle versions fetch completion."""
self._available_versions = versions
self.version_combo.clear()
self.version_combo.addItem("Latest", None)
for version in versions:
self.version_combo.addItem(f"v{version}", version)
self.version_combo.setEnabled(len(versions) > 1)
def _on_versions_error(self, error: str):
"""Handle versions fetch error."""
self._available_versions = []
self.version_combo.clear()
self.version_combo.addItem("Latest", None)
self.version_combo.setEnabled(False)
def _update_install_buttons(self):
"""Update install/update buttons based on selection."""
if not self._selected_tool:
return
tool_name = self._selected_tool.get("name", "")
if tool_name in self._installed_tools:
installed_version = self._installed_tools[tool_name]
registry_version = tool.get("version", "1.0.0")
registry_version = self._selected_tool.get("version", "1.0.0")
if installed_version != registry_version:
self.btn_install.hide()
self.btn_update.show()
@@ -546,12 +625,16 @@ class RegistryPage(QWidget):
owner = self._selected_tool.get("owner", "")
name = self._selected_tool.get("name", "")
version = self.version_combo.currentData() # None for "Latest"
self.btn_install.setEnabled(False)
self.btn_update.setEnabled(False)
self.status_label.setText(f"Installing {owner}/{name}...")
self.version_combo.setEnabled(False)
self._install_worker = InstallWorker(owner, name)
version_str = f"@{version}" if version else ""
self.status_label.setText(f"Installing {owner}/{name}{version_str}...")
self._install_worker = InstallWorker(owner, name, version)
self._install_worker.finished.connect(self._on_install_complete)
self._install_worker.error.connect(self._on_install_error)
self._install_worker.start()