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:
rob
2026-01-16 06:20:16 -04:00
co-authored by Claude Opus 4.5
parent 78025aac8e
commit 5e01a335c0
11 changed files with 1072 additions and 74 deletions
+106 -3
View File
@@ -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 %}