Fix publish state tracking and add sync status feature

- Track moderation status (pending/approved/rejected) in local tool config
- Add new "pending" state indicator (◐ yellow) distinct from "published" (✓ green)
- Add "Sync Status" button to check registry for updated moderation status
- Add /me/tools/<name>/status API endpoint for checking tool status
- Improve admin panel error handling with better error messages

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rob 2026-01-16 07:52:42 -04:00
parent 3fccf854be
commit f1c462146b
5 changed files with 151 additions and 11 deletions

View File

@ -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

View File

@ -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(
"<p style='background: #c6f6d5; color: #276749; padding: 6px 10px; "
"border-radius: 4px; margin-bottom: 12px; font-size: 12px;'>"
"✓ Published to registry - up to date</p>"
"✓ Published to registry - approved</p>"
)
elif state == "pending":
lines.append(
"<p style='background: #fef3c7; color: #92400e; padding: 6px 10px; "
"border-radius: 4px; margin-bottom: 12px; font-size: 12px;'>"
"◐ Submitted to registry - pending review</p>"
)
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}")

View File

@ -1900,6 +1900,38 @@ def create_app() -> Flask:
})
return jsonify({"data": data})
@app.route("/api/v1/me/tools/<name>/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/<owner>/<name>/deprecate", methods=["POST"])
@require_token
def deprecate_tool(owner: str, name: str) -> Response:

View File

@ -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.

View File

@ -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'));
}
}
</script>