Add registry improvements, tag filtering, and admin tools
Registry API improvements: - Add /api/v1/me POST endpoint for profile updates - Add /api/v1/me/password POST endpoint for password changes - Fix visibility filtering on categories and tags endpoints to only show approved public tools - Allow underscores in tool names for fabric pattern compatibility - Fix sqlite3.Row access (use bracket notation instead of .get()) Web UI improvements: - Add tag filter to /tools page with three-state buttons (include/exclude/neutral) - Add mobile-friendly tag filter in responsive view - Display tags on tool cards in the tools listing - Add dashboard settings form handlers for profile and password Admin improvements: - Add scrutiny audit page for reviewing tool safety analysis - Improve pending tools page with scrutiny report display - Add scrutiny stats to admin dashboard Fabric sync improvements: - Add direct database publishing with scrutiny vetting - Support auto-approve for tools passing scrutiny - Improve error handling and logging - Add source attribution for imported tools Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
@@ -67,6 +67,43 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Tags Filter -->
|
||||
{% if all_tags %}
|
||||
<div>
|
||||
<button type="button"
|
||||
onclick="toggleTagsFilter()"
|
||||
class="flex items-center justify-between w-full text-sm font-medium text-gray-700 mb-3">
|
||||
<span>Tags</span>
|
||||
<svg id="tags-chevron" class="w-4 h-4 text-gray-500 transition-transform {{ '' if include_tags or exclude_tags else 'rotate-180' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div id="tags-filter" class="{{ '' if include_tags or exclude_tags else 'hidden' }}">
|
||||
{% if include_tags or exclude_tags %}
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-xs text-gray-500">Click to cycle: include/exclude/none</span>
|
||||
<a href="{{ url_for('web.tools', sort=sort, category=current_category) }}" class="text-xs text-indigo-600 hover:underline">Clear</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-xs text-gray-500 mb-2">Click to cycle: include/exclude/none</p>
|
||||
{% endif %}
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% for tag in all_tags[:20] %}
|
||||
{% set tag_state = 'include' if tag.name in include_tags else ('exclude' if tag.name in exclude_tags else 'neutral') %}
|
||||
<button type="button"
|
||||
onclick="cycleTagState('{{ tag.name }}', '{{ tag_state }}')"
|
||||
class="tag-btn px-2 py-1 text-xs rounded-full border transition-colors
|
||||
{% if tag_state == 'include' %}bg-green-100 border-green-300 text-green-700
|
||||
{% elif tag_state == 'exclude' %}bg-red-100 border-red-300 text-red-700 line-through
|
||||
{% else %}bg-white border-gray-300 text-gray-600 hover:border-gray-400{% endif %}">
|
||||
{{ tag.name }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Sort -->
|
||||
<div>
|
||||
<label for="sort-select" class="block text-sm font-medium text-gray-700 mb-3">Sort By</label>
|
||||
@@ -133,6 +170,29 @@
|
||||
<option value="name" {{ 'selected' if sort == 'name' }}>Name (A-Z)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Tags Filter -->
|
||||
{% if all_tags %}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Tags</label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% for tag in all_tags[:12] %}
|
||||
{% set tag_state = 'include' if tag.name in include_tags else ('exclude' if tag.name in exclude_tags else 'neutral') %}
|
||||
<button type="button"
|
||||
onclick="cycleTagState('{{ tag.name }}', '{{ tag_state }}')"
|
||||
class="px-2 py-1 text-xs rounded-full border transition-colors
|
||||
{% if tag_state == 'include' %}bg-green-100 border-green-300 text-green-700
|
||||
{% elif tag_state == 'exclude' %}bg-red-100 border-red-300 text-red-700 line-through
|
||||
{% else %}bg-white border-gray-300 text-gray-600{% endif %}">
|
||||
{{ tag.name }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if include_tags or exclude_tags %}
|
||||
<a href="{{ url_for('web.tools', sort=sort, category=current_category) }}" class="text-xs text-indigo-600 hover:underline mt-2 inline-block">Clear tags</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -160,7 +220,8 @@
|
||||
description=tool.description,
|
||||
category=tool.category,
|
||||
downloads=tool.downloads,
|
||||
version=tool.version
|
||||
version=tool.version,
|
||||
tags=tool.tags
|
||||
) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -170,7 +231,7 @@
|
||||
<nav class="mt-12 flex items-center justify-center" aria-label="Pagination">
|
||||
<div class="flex items-center gap-2">
|
||||
{% if pagination.has_prev %}
|
||||
<a href="{{ url_for('web.tools', page=pagination.prev_num, sort=sort, category=current_category) }}"
|
||||
<a href="{{ url_for('web.tools', page=pagination.prev_num, sort=sort, category=current_category, include_tags=include_tags|join(',') if include_tags else none, exclude_tags=exclude_tags|join(',') if exclude_tags else none) }}"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
|
||||
Previous
|
||||
</a>
|
||||
@@ -181,7 +242,7 @@
|
||||
</span>
|
||||
|
||||
{% if pagination.has_next %}
|
||||
<a href="{{ url_for('web.tools', page=pagination.next_num, sort=sort, category=current_category) }}"
|
||||
<a href="{{ url_for('web.tools', page=pagination.next_num, sort=sort, category=current_category, include_tags=include_tags|join(',') if include_tags else none, exclude_tags=exclude_tags|join(',') if exclude_tags else none) }}"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
|
||||
Next
|
||||
</a>
|
||||
@@ -228,5 +289,47 @@ function updateSort(value) {
|
||||
url.searchParams.set('sort', value);
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
|
||||
function toggleTagsFilter() {
|
||||
const panel = document.getElementById('tags-filter');
|
||||
const chevron = document.getElementById('tags-chevron');
|
||||
panel.classList.toggle('hidden');
|
||||
chevron.classList.toggle('rotate-180');
|
||||
}
|
||||
|
||||
function cycleTagState(tagName, currentState) {
|
||||
const url = new URL(window.location.href);
|
||||
let includeTags = (url.searchParams.get('include_tags') || '').split(',').filter(Boolean);
|
||||
let excludeTags = (url.searchParams.get('exclude_tags') || '').split(',').filter(Boolean);
|
||||
|
||||
// Remove from both lists first
|
||||
includeTags = includeTags.filter(t => t !== tagName);
|
||||
excludeTags = excludeTags.filter(t => t !== tagName);
|
||||
|
||||
// Cycle: neutral -> include -> exclude -> neutral
|
||||
if (currentState === 'neutral') {
|
||||
includeTags.push(tagName);
|
||||
} else if (currentState === 'include') {
|
||||
excludeTags.push(tagName);
|
||||
}
|
||||
// else: exclude -> neutral (already removed)
|
||||
|
||||
// Update URL params
|
||||
if (includeTags.length > 0) {
|
||||
url.searchParams.set('include_tags', includeTags.join(','));
|
||||
} else {
|
||||
url.searchParams.delete('include_tags');
|
||||
}
|
||||
if (excludeTags.length > 0) {
|
||||
url.searchParams.set('exclude_tags', excludeTags.join(','));
|
||||
} else {
|
||||
url.searchParams.delete('exclude_tags');
|
||||
}
|
||||
|
||||
// Reset to page 1 when filters change
|
||||
url.searchParams.delete('page');
|
||||
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user