Add fork display, version selector, and admin cleanup features
Registry Features: - Fork tracking with forked_from/forked_version metadata - Forked tools show "Forked from" notice on detail page - Original tools display "Forks" section listing all forks - Fork count in tool stats - API: GET /api/v1/tools/<owner>/<name>/forks GUI Improvements: - Version selector dropdown for registry installs - Fetch available versions via GET /api/v1/tools/<owner>/<name>/versions - "Latest" option plus all available versions listed Admin Features: - POST /api/v1/admin/cleanup/rejected endpoint - Maintenance section in admin dashboard - "Dry Run" and "Run Cleanup" buttons for rejected version cleanup - Configurable grace period (default 7 days) Documentation: - Updated CHANGELOG.md with all recent changes Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
@@ -106,5 +106,135 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Maintenance Section (Admin only) -->
|
||||
{% if user.role == 'admin' %}
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">Maintenance</h3>
|
||||
<div class="space-y-4">
|
||||
<!-- Cleanup Rejected Versions -->
|
||||
<div class="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 bg-red-100 rounded-lg p-2 mr-4">
|
||||
<svg class="h-5 w-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-700">Cleanup Rejected Versions</p>
|
||||
<p class="text-xs text-gray-500">Delete rejected tool versions older than 7 days</p>
|
||||
<p class="text-xs text-gray-400 mt-1">
|
||||
<span id="rejected-count">{{ rejected_count }}</span> rejected version(s) in registry
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button onclick="runCleanup(true)" class="px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
|
||||
Dry Run
|
||||
</button>
|
||||
<button onclick="runCleanup(false)" class="px-3 py-2 text-sm font-medium text-white bg-red-600 rounded-md hover:bg-red-700">
|
||||
Run Cleanup
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Cleanup Result Modal -->
|
||||
<div id="cleanup-modal" class="fixed inset-0 bg-gray-900 bg-opacity-60 hidden overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto border-2 border-gray-300 w-[32rem] shadow-2xl rounded-lg bg-white">
|
||||
<div class="bg-gray-50 px-5 py-4 rounded-t-lg border-b border-gray-200">
|
||||
<h3 id="cleanup-modal-title" class="text-lg font-medium text-gray-900">Cleanup Results</h3>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<div id="cleanup-loading" class="text-center py-4">
|
||||
<svg class="animate-spin h-8 w-8 text-indigo-600 mx-auto" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<p class="mt-2 text-sm text-gray-600">Running cleanup...</p>
|
||||
</div>
|
||||
<div id="cleanup-result" class="hidden">
|
||||
<div id="cleanup-summary" class="mb-4 p-3 rounded-lg bg-gray-50"></div>
|
||||
<div id="cleanup-list" class="max-h-64 overflow-y-auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-5 py-4 rounded-b-lg flex justify-end">
|
||||
<button onclick="closeCleanupModal()" class="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-500">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function runCleanup(dryRun) {
|
||||
document.getElementById('cleanup-modal').classList.remove('hidden');
|
||||
document.getElementById('cleanup-loading').classList.remove('hidden');
|
||||
document.getElementById('cleanup-result').classList.add('hidden');
|
||||
document.getElementById('cleanup-modal-title').textContent = dryRun ? 'Cleanup Preview (Dry Run)' : 'Cleanup Results';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/v1/admin/cleanup/rejected?days=7&dry_run=${dryRun}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer {{ session.get("auth_token") }}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error?.message || 'Cleanup failed');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
const data = result.data;
|
||||
|
||||
document.getElementById('cleanup-loading').classList.add('hidden');
|
||||
document.getElementById('cleanup-result').classList.remove('hidden');
|
||||
|
||||
// Summary
|
||||
const summaryHtml = `
|
||||
<p class="text-sm">
|
||||
<span class="font-medium">${data.deleted_count}</span> rejected version(s)
|
||||
${data.dry_run ? 'would be' : 'were'} deleted
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 mt-1">Cutoff: ${data.cutoff_date.split('T')[0]}</p>
|
||||
${data.dry_run ? '<p class="text-xs text-yellow-600 mt-2">This was a dry run. No tools were actually deleted.</p>' : ''}
|
||||
`;
|
||||
document.getElementById('cleanup-summary').innerHTML = summaryHtml;
|
||||
|
||||
// List of deleted tools
|
||||
if (data.deleted_tools && data.deleted_tools.length > 0) {
|
||||
const listHtml = '<ul class="space-y-2">' + data.deleted_tools.map(tool => `
|
||||
<li class="text-sm p-2 bg-white rounded border">
|
||||
<span class="font-medium">${tool.name}</span>
|
||||
<span class="text-gray-500">v${tool.version}</span>
|
||||
<span class="text-xs text-gray-400 ml-2">rejected ${tool.moderated_at?.split('T')[0] || 'unknown'}</span>
|
||||
</li>
|
||||
`).join('') + '</ul>';
|
||||
document.getElementById('cleanup-list').innerHTML = listHtml;
|
||||
} else {
|
||||
document.getElementById('cleanup-list').innerHTML = '<p class="text-sm text-gray-500">No rejected versions found older than 7 days.</p>';
|
||||
}
|
||||
|
||||
// Update the count display if not dry run
|
||||
if (!data.dry_run) {
|
||||
const newCount = Math.max(0, parseInt(document.getElementById('rejected-count').textContent) - data.deleted_count);
|
||||
document.getElementById('rejected-count').textContent = newCount;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
document.getElementById('cleanup-loading').classList.add('hidden');
|
||||
document.getElementById('cleanup-result').classList.remove('hidden');
|
||||
document.getElementById('cleanup-summary').innerHTML = `<p class="text-sm text-red-600">Error: ${error.message}</p>`;
|
||||
document.getElementById('cleanup-list').innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
function closeCleanupModal() {
|
||||
document.getElementById('cleanup-modal').classList.add('hidden');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user