Add tool detail view to admin pending page

- Click tool name to view full details in modal
- Shows description, arguments, steps (prompt/code), and README
- Approve/Reject buttons in detail modal
- New API endpoint GET /api/v1/admin/tools/<id> returns full tool config

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rob 2026-01-16 14:55:50 -04:00
parent 706accc008
commit 3914ca74b3
2 changed files with 203 additions and 1 deletions

View File

@ -2459,6 +2459,59 @@ def create_app() -> Flask:
"meta": paginate(page, per_page, total),
})
@app.route("/api/v1/admin/tools/<int:tool_id>", methods=["GET"])
@require_moderator
def admin_get_tool(tool_id: int) -> Response:
"""Get full tool details for admin review."""
tool = query_one(
g.db,
"""
SELECT t.*, p.display_name as publisher_name
FROM tools t
JOIN publishers p ON t.publisher_id = p.id
WHERE t.id = ?
""",
[tool_id],
)
if not tool:
return error_response("TOOL_NOT_FOUND", "Tool not found", 404)
# Parse config YAML to extract steps
config = {}
if tool["config_yaml"]:
try:
config = yaml.safe_load(tool["config_yaml"]) or {}
except yaml.YAMLError:
pass
# Parse scrutiny report
scrutiny_report = None
if tool["scrutiny_report"]:
try:
scrutiny_report = json.loads(tool["scrutiny_report"])
except (json.JSONDecodeError, TypeError):
pass
return jsonify({
"data": {
"id": tool["id"],
"owner": tool["owner"],
"name": tool["name"],
"version": tool["version"],
"description": tool["description"],
"category": tool["category"],
"tags": tool["tags"],
"published_at": tool["published_at"],
"publisher_name": tool["publisher_name"],
"visibility": tool["visibility"],
"moderation_status": tool["moderation_status"],
"scrutiny_status": tool["scrutiny_status"],
"scrutiny_report": scrutiny_report,
"config": config,
"readme": tool["readme"] or "",
}
})
@app.route("/api/v1/admin/tools/<int:tool_id>/approve", methods=["POST"])
@require_moderator
def admin_approve_tool(tool_id: int) -> Response:

View File

@ -30,7 +30,9 @@
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div>
<div class="text-sm font-medium text-gray-900">{{ tool.owner }}/{{ tool.name }}</div>
<button onclick="viewTool({{ tool.id }})" class="text-sm font-medium text-indigo-600 hover:text-indigo-800 hover:underline">
{{ tool.owner }}/{{ tool.name }}
</button>
<div class="text-sm text-gray-500">v{{ tool.version }}</div>
</div>
</div>
@ -137,6 +139,51 @@
{% endif %}
</div>
<!-- Tool Detail Modal -->
<div id="detail-modal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50">
<div class="relative top-10 mx-auto p-6 border max-w-4xl shadow-lg rounded-md bg-white mb-10">
<div class="flex items-center justify-between mb-4">
<h3 id="detail-title" class="text-lg font-medium text-gray-900">Tool Details</h3>
<button onclick="closeDetailModal()" class="text-gray-400 hover:text-gray-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div id="detail-loading" class="py-8 text-center text-gray-500">Loading...</div>
<div id="detail-content" class="hidden">
<!-- Description -->
<div class="mb-6">
<h4 class="text-sm font-medium text-gray-700 mb-2">Description</h4>
<p id="detail-description" class="text-sm text-gray-600"></p>
</div>
<!-- Arguments -->
<div id="detail-args-section" class="mb-6">
<h4 class="text-sm font-medium text-gray-700 mb-2">Arguments</h4>
<div id="detail-args" class="bg-gray-50 rounded-md p-3 text-sm font-mono overflow-x-auto"></div>
</div>
<!-- Steps -->
<div id="detail-steps-section" class="mb-6">
<h4 class="text-sm font-medium text-gray-700 mb-2">Steps</h4>
<div id="detail-steps" class="space-y-3"></div>
</div>
<!-- README -->
<div id="detail-readme-section" class="mb-6">
<h4 class="text-sm font-medium text-gray-700 mb-2">README</h4>
<div id="detail-readme" class="bg-gray-50 rounded-md p-4 text-sm prose prose-sm max-w-none overflow-auto max-h-64"></div>
</div>
</div>
<div class="mt-6 flex justify-end space-x-3 border-t pt-4">
<button onclick="closeDetailModal()" class="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-500">Close</button>
<button id="detail-approve-btn" class="px-4 py-2 bg-green-600 text-white text-sm font-medium rounded-md hover:bg-green-700">Approve</button>
<button id="detail-reject-btn" class="px-4 py-2 bg-red-600 text-white text-sm font-medium rounded-md hover:bg-red-700">Reject</button>
</div>
</div>
</div>
<!-- Reject Modal -->
<div id="reject-modal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50">
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
@ -153,12 +200,114 @@
<script>
let currentToolId = null;
let currentDetailToolId = null;
function toggleFindings(toolId) {
const el = document.getElementById(`findings-${toolId}`);
el.classList.toggle('hidden');
}
async function viewTool(toolId) {
currentDetailToolId = toolId;
document.getElementById('detail-modal').classList.remove('hidden');
document.getElementById('detail-loading').classList.remove('hidden');
document.getElementById('detail-content').classList.add('hidden');
try {
const response = await fetch(`/api/v1/admin/tools/${toolId}`, {
headers: {
'Authorization': 'Bearer {{ session.get("auth_token") }}'
}
});
if (!response.ok) {
throw new Error('Failed to load tool details');
}
const result = await response.json();
const tool = result.data;
// Update title
document.getElementById('detail-title').textContent = `${tool.owner}/${tool.name} v${tool.version}`;
// Description
document.getElementById('detail-description').textContent = tool.description || 'No description';
// Arguments
const argsSection = document.getElementById('detail-args-section');
const argsDiv = document.getElementById('detail-args');
if (tool.config.arguments && tool.config.arguments.length > 0) {
argsSection.classList.remove('hidden');
argsDiv.innerHTML = tool.config.arguments.map(arg => {
const required = arg.required ? '<span class="text-red-600">*</span>' : '';
return `<div class="mb-2"><span class="text-indigo-600">${arg.flag || arg.variable}</span>${required} - ${arg.description || 'No description'}</div>`;
}).join('');
} else {
argsSection.classList.add('hidden');
}
// Steps
const stepsSection = document.getElementById('detail-steps-section');
const stepsDiv = document.getElementById('detail-steps');
if (tool.config.steps && tool.config.steps.length > 0) {
stepsSection.classList.remove('hidden');
stepsDiv.innerHTML = tool.config.steps.map((step, idx) => {
let content = '';
if (step.type === 'prompt') {
content = `<div class="text-xs text-gray-500 mb-1">Prompt Step</div>
<pre class="bg-white p-2 rounded border text-xs overflow-x-auto whitespace-pre-wrap">${escapeHtml(step.content || '')}</pre>`;
} else if (step.type === 'code') {
content = `<div class="text-xs text-gray-500 mb-1">Code Step${step.output_var ? ` → ${step.output_var}` : ''}</div>
<pre class="bg-gray-900 text-green-400 p-2 rounded text-xs overflow-x-auto"><code>${escapeHtml(step.code || '')}</code></pre>`;
} else {
content = `<div class="text-xs text-gray-500">${step.type} step</div>`;
}
return `<div class="bg-gray-50 p-3 rounded-md">
<div class="text-xs font-medium text-gray-700 mb-2">Step ${idx + 1}</div>
${content}
</div>`;
}).join('');
} else {
stepsSection.classList.add('hidden');
}
// README
const readmeSection = document.getElementById('detail-readme-section');
const readmeDiv = document.getElementById('detail-readme');
if (tool.readme) {
readmeSection.classList.remove('hidden');
readmeDiv.innerHTML = `<pre class="whitespace-pre-wrap text-xs">${escapeHtml(tool.readme)}</pre>`;
} else {
readmeSection.classList.add('hidden');
}
// Wire up approve/reject buttons
document.getElementById('detail-approve-btn').onclick = () => {
closeDetailModal();
approveTool(toolId);
};
document.getElementById('detail-reject-btn').onclick = () => {
closeDetailModal();
rejectTool(toolId);
};
document.getElementById('detail-loading').classList.add('hidden');
document.getElementById('detail-content').classList.remove('hidden');
} catch (error) {
console.error('Error loading tool:', error);
document.getElementById('detail-loading').textContent = 'Error loading tool details';
}
}
function closeDetailModal() {
document.getElementById('detail-modal').classList.add('hidden');
currentDetailToolId = null;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
async function approveTool(toolId) {
if (!confirm('Approve this tool?')) return;
try {