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:
parent
3fccf854be
commit
f1c462146b
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue