diff --git a/src/cmdforge/gui/dialogs/publish_dialog.py b/src/cmdforge/gui/dialogs/publish_dialog.py index c17c0dd..6fd1316 100644 --- a/src/cmdforge/gui/dialogs/publish_dialog.py +++ b/src/cmdforge/gui/dialogs/publish_dialog.py @@ -183,14 +183,16 @@ class PublishDialog(QDialog): self.status_label.setText("Published successfully!") self.status_label.setStyleSheet("color: #38a169; font-weight: 600;") - # Save registry_hash to local config for publish state tracking + # Save registry_hash and moderation status to local config for publish state tracking config_hash = result.get("config_hash") + moderation_status = result.get("status", "pending") if config_hash and hasattr(self._tool, 'path') and self._tool.path: try: config_path = self._tool.path if config_path.exists(): config_data = yaml.safe_load(config_path.read_text()) or {} config_data["registry_hash"] = config_hash + config_data["registry_status"] = moderation_status config_path.write_text(yaml.dump(config_data, default_flow_style=False, sort_keys=False)) except Exception: pass # Non-critical - just won't show publish state diff --git a/src/cmdforge/gui/pages/tools_page.py b/src/cmdforge/gui/pages/tools_page.py index ff70392..4633980 100644 --- a/src/cmdforge/gui/pages/tools_page.py +++ b/src/cmdforge/gui/pages/tools_page.py @@ -29,7 +29,8 @@ def get_tool_publish_state(tool_name: str) -> Tuple[str, Optional[str]]: Returns: Tuple of (state, registry_hash) where state is: - - "published" - has registry_hash and current hash matches + - "published" - approved in registry and current hash matches + - "pending" - submitted but awaiting moderation - "modified" - has registry_hash but current hash differs - "local" - no registry_hash (never published/downloaded) """ @@ -40,6 +41,7 @@ def get_tool_publish_state(tool_name: str) -> Tuple[str, Optional[str]]: try: config = yaml.safe_load(config_path.read_text()) registry_hash = config.get("registry_hash") + registry_status = config.get("registry_status", "pending") if not registry_hash: return ("local", None) @@ -47,10 +49,15 @@ def get_tool_publish_state(tool_name: str) -> Tuple[str, Optional[str]]: # Compute current hash (excluding hash fields) current_hash = compute_config_hash(config) - if current_hash == registry_hash: + if current_hash != registry_hash: + return ("modified", registry_hash) + + # Hash matches - check moderation status + if registry_status == "approved": return ("published", registry_hash) else: - return ("modified", registry_hash) + # pending, rejected, or unknown + return ("pending", registry_hash) except Exception: return ("local", None) @@ -156,6 +163,14 @@ class ToolsPage(QWidget): btn_layout.addStretch() + # Sync status button (only when connected) + self.btn_sync = QPushButton("Sync Status") + self.btn_sync.setObjectName("secondary") + self.btn_sync.clicked.connect(self._sync_tool_status) + self.btn_sync.setEnabled(False) + self.btn_sync.setToolTip("Check registry for updated moderation status") + btn_layout.addWidget(self.btn_sync) + # Connect/Publish button config = load_config() if config.registry.token: @@ -219,8 +234,12 @@ class ToolsPage(QWidget): # Show state indicator in display name if state == "published": display_name = f"{name} ✓" - tooltip = "Published to registry - up to date" + tooltip = "Published to registry - approved" color = QColor(56, 161, 105) # Green + elif state == "pending": + display_name = f"{name} ◐" + tooltip = "Submitted to registry - pending review" + color = QColor(214, 158, 46) # Yellow/amber elif state == "modified": display_name = f"{name} ●" tooltip = "Published to registry - local modifications" @@ -298,7 +317,13 @@ class ToolsPage(QWidget): lines.append( "

" - "✓ Published to registry - up to date

" + "✓ Published to registry - approved

" + ) + elif state == "pending": + lines.append( + "

" + "◐ Submitted to registry - pending review

" ) elif state == "modified": lines.append( @@ -360,9 +385,16 @@ class ToolsPage(QWidget): if config.registry.token: # Connected - enable Publish when tool selected self.btn_publish.setEnabled(has_selection) + # Enable Sync when tool selected and has registry_hash + if has_selection: + state, registry_hash = get_tool_publish_state(self._current_tool.name) + self.btn_sync.setEnabled(registry_hash is not None) + else: + self.btn_sync.setEnabled(False) else: # Not connected - Connect button is always enabled self.btn_publish.setEnabled(True) + self.btn_sync.setEnabled(False) def _create_tool(self): """Create a new tool.""" @@ -413,8 +445,54 @@ class ToolsPage(QWidget): if not self._current_tool: return + tool_name = self._current_tool.name # Save before refresh clears it from ..dialogs.publish_dialog import PublishDialog dialog = PublishDialog(self, self._current_tool) if dialog.exec(): self.refresh() # Refresh to show updated publish state indicator - self.main_window.show_status(f"Published '{self._current_tool.name}'") + self.main_window.show_status(f"Published '{tool_name}'") + + def _sync_tool_status(self): + """Sync the moderation status of the selected tool from the registry.""" + if not self._current_tool: + return + + tool_name = self._current_tool.name + config_path = get_tools_dir() / tool_name / "config.yaml" + + if not config_path.exists(): + return + + try: + from ...registry_client import RegistryClient, RegistryError + + config = load_config() + client = RegistryClient() + client.token = config.registry.token + + # Get status from registry + status_data = client.get_my_tool_status(tool_name) + new_status = status_data.get("status", "pending") + + # Update local config + config_data = yaml.safe_load(config_path.read_text()) or {} + old_status = config_data.get("registry_status", "pending") + + if old_status != new_status: + config_data["registry_status"] = new_status + config_path.write_text(yaml.dump(config_data, default_flow_style=False, sort_keys=False)) + + self.refresh() + if new_status == "approved": + self.main_window.show_status(f"Tool '{tool_name}' has been approved!") + elif new_status == "rejected": + self.main_window.show_status(f"Tool '{tool_name}' was rejected") + else: + self.main_window.show_status(f"Status updated: {new_status}") + else: + self.main_window.show_status(f"Status unchanged: {new_status}") + + except RegistryError as e: + QMessageBox.warning(self, "Sync Error", f"Could not sync status:\n{e.message}") + except Exception as e: + QMessageBox.warning(self, "Sync Error", f"Could not sync status:\n{e}") diff --git a/src/cmdforge/registry/app.py b/src/cmdforge/registry/app.py index 91bbdd8..daa13dd 100644 --- a/src/cmdforge/registry/app.py +++ b/src/cmdforge/registry/app.py @@ -1900,6 +1900,38 @@ def create_app() -> Flask: }) return jsonify({"data": data}) + @app.route("/api/v1/me/tools//status", methods=["GET"]) + @require_token + def my_tool_status(name: str) -> Response: + """Get the moderation status of a specific tool owned by the current user.""" + owner = g.current_publisher["slug"] + + # Query for the most recent version of this tool + row = query_one( + g.db, + """ + SELECT name, version, moderation_status, config_hash, published_at + FROM tools + WHERE owner = ? AND name = ? + ORDER BY published_at DESC + LIMIT 1 + """, + [owner, name], + ) + + if not row: + return error_response("TOOL_NOT_FOUND", f"Tool '{name}' not found", 404) + + return jsonify({ + "data": { + "name": row["name"], + "version": row["version"], + "status": row["moderation_status"], + "config_hash": row["config_hash"], + "published_at": row["published_at"], + } + }) + @app.route("/api/v1/tools///deprecate", methods=["POST"]) @require_token def deprecate_tool(owner: str, name: str) -> Response: diff --git a/src/cmdforge/registry_client.py b/src/cmdforge/registry_client.py index ee9f313..185c5af 100644 --- a/src/cmdforge/registry_client.py +++ b/src/cmdforge/registry_client.py @@ -620,6 +620,30 @@ class RegistryClient: tools = response.json().get("data", []) return [ToolInfo.from_dict(t) for t in tools] + def get_my_tool_status(self, name: str) -> Dict[str, Any]: + """ + Get the moderation status of a specific tool owned by the authenticated user. + + Args: + name: Tool name + + Returns: + Dict with name, version, status (pending/approved/rejected), config_hash, published_at + """ + response = self._request("GET", f"/me/tools/{name}/status", require_auth=True) + + if response.status_code == 404: + raise RegistryError( + code="TOOL_NOT_FOUND", + message=f"Tool '{name}' not found", + http_status=404 + ) + + if response.status_code != 200: + self._handle_error_response(response) + + return response.json().get("data", {}) + def get_popular_tools(self, limit: int = 10) -> List[ToolInfo]: """ Get most popular tools. diff --git a/src/cmdforge/web/templates/admin/pending.html b/src/cmdforge/web/templates/admin/pending.html index a575458..893f701 100644 --- a/src/cmdforge/web/templates/admin/pending.html +++ b/src/cmdforge/web/templates/admin/pending.html @@ -145,14 +145,16 @@ async function approveTool(toolId) { } }); if (response.ok) { - document.getElementById(`tool-row-${toolId}`).remove(); + const row = document.getElementById(`tool-row-${toolId}`); + if (row) row.remove(); location.reload(); } else { const data = await response.json(); alert(data.error?.message || 'Failed to approve tool'); } } catch (error) { - alert('Network error'); + console.error('Approve error:', error); + alert('Error: ' + (error.message || 'Unknown error occurred')); } } @@ -184,14 +186,16 @@ async function confirmReject() { }); if (response.ok) { closeRejectModal(); - document.getElementById(`tool-row-${currentToolId}`).remove(); + const row = document.getElementById(`tool-row-${currentToolId}`); + if (row) row.remove(); location.reload(); } else { const data = await response.json(); alert(data.error?.message || 'Failed to reject tool'); } } catch (error) { - alert('Network error'); + console.error('Reject error:', error); + alert('Error: ' + (error.message || 'Unknown error occurred')); } }