Fix publish flow and consolidate My Tools view

- Make publish endpoint idempotent: return success if same content
  already exists instead of VERSION_EXISTS error (fixes retry issues)
- Always refresh tools page after publish dialog closes
- Consolidate My Tools page to group versions by tool name
- Show version count and list instead of separate rows per version
- Sum downloads across all versions for accurate totals

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rob 2026-01-16 23:17:55 -04:00
parent 16de3e371c
commit 74b5124360
4 changed files with 60 additions and 7 deletions

View File

@ -648,8 +648,11 @@ class ToolsPage(QWidget):
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
result = dialog.exec()
# Always refresh after dialog closes - tool config may have been updated
# even if user canceled (e.g., server processed request before client timeout)
self.refresh()
if result:
self.main_window.show_status(f"Published '{tool_name}'")
def _sync_tool_status(self):

View File

@ -1797,15 +1797,34 @@ def create_app() -> Flask:
return error_response("VALIDATION_ERROR", "Tag exceeds 32 characters")
owner = g.current_publisher["slug"]
# Compute config hash early for idempotency check
config_hash = compute_yaml_hash(config_text)
existing = query_one(
g.db,
"SELECT published_at FROM tools WHERE owner = ? AND name = ? AND version = ?",
"SELECT published_at, config_hash, moderation_status, visibility FROM tools WHERE owner = ? AND name = ? AND version = ?",
[owner, name, version],
)
if existing:
# If same content (config_hash matches), treat as idempotent retry - return success
if existing["config_hash"] == config_hash:
return jsonify({
"data": {
"owner": owner,
"name": name,
"version": version,
"config_hash": config_hash,
"pr_url": "",
"status": existing["moderation_status"],
"visibility": existing["visibility"],
"already_published": True,
}
})
# Different content - actual version conflict
return error_response(
"VERSION_EXISTS",
f"Version {version} already exists",
f"Version {version} already exists with different content. Use a new version number.",
409,
details={"published_at": existing["published_at"]},
)
@ -1895,8 +1914,7 @@ def create_app() -> Flask:
tags_json = json.dumps(tags)
# Compute content hash for integrity verification
config_hash = compute_yaml_hash(config_text)
# config_hash already computed earlier for idempotency check
# Determine status based on scrutiny
if scrutiny_report and scrutiny_report.get("decision") == "approve":

View File

@ -602,7 +602,32 @@ def dashboard_tools():
token = session.get("auth_token")
user = _load_current_publisher() or session.get("user", {})
status, payload = _api_get("/api/v1/me/tools", token=token)
tools = payload.get("data", []) if status == 200 else []
all_tools = payload.get("data", []) if status == 200 else []
# Consolidate multiple versions into single tool entries
# Group by tool name, keep latest version as main, collect all versions
tools_by_name = {}
for tool in all_tools:
name = tool.get("name")
if name not in tools_by_name:
tools_by_name[name] = {
**tool,
"versions": [tool.get("version")],
"all_downloads": tool.get("downloads", 0),
}
else:
# Add this version to the list
tools_by_name[name]["versions"].append(tool.get("version"))
tools_by_name[name]["all_downloads"] += tool.get("downloads", 0)
# Keep the latest version's data (assuming sorted by published_at desc)
# The first one we saw is the latest, so no need to update
# Convert back to list and sort versions for display
tools = list(tools_by_name.values())
for tool in tools:
tool["versions"].sort(key=lambda v: [int(x) for x in v.split(".")], reverse=True)
tool["version_count"] = len(tool["versions"])
tool["downloads"] = tool["all_downloads"] # Use total across all versions
# Load rating/issue data for each tool
for tool in tools:

View File

@ -69,7 +69,14 @@
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="text-sm text-gray-900">v{{ tool.version }}</span>
{% if tool.version_count and tool.version_count > 1 %}
<span class="ml-1 text-xs text-gray-500">(+{{ tool.version_count - 1 }} more)</span>
<div class="text-xs text-gray-400 mt-1">
{{ tool.versions[:3]|join(', ') }}{% if tool.version_count > 3 %}...{% endif %}
</div>
{% else %}
<p class="text-xs text-gray-500">{{ tool.published_at|timeago }}</p>
{% endif %}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{{ tool.downloads|format_number }}