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
@@ -83,6 +83,12 @@
</svg>
<span class="text-sm font-medium text-gray-700">Manage publishers</span>
</a>
<a href="{{ url_for('web.admin_scrutiny') }}" class="flex items-center p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
<svg class="h-5 w-5 text-gray-400 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
</svg>
<span class="text-sm font-medium text-gray-700">Scrutiny audit</span>
</a>
{% if user.role == 'admin' %}
<a href="{{ url_for('web.admin_settings') }}" class="flex items-center p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
<svg class="h-5 w-5 text-gray-400 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+38 -4
View File
@@ -19,7 +19,7 @@
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tool</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Publisher</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Category</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Scrutiny</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Submitted</th>
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
@@ -37,11 +37,40 @@
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900">{{ tool.publisher_name }}</div>
<div class="text-xs text-gray-500">{{ tool.category or 'Uncategorized' }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800">
{{ tool.category or 'Uncategorized' }}
<td class="px-6 py-4">
{% if tool.scrutiny_status == 'approved' %}
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
Passed
</span>
{% elif tool.scrutiny_status == 'pending_review' %}
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800">
Warnings
</span>
{% else %}
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800">
{{ tool.scrutiny_status or 'N/A' }}
</span>
{% endif %}
{% if tool.scrutiny_report and tool.scrutiny_report.findings %}
<button onclick="toggleFindings({{ tool.id }})" class="ml-2 text-xs text-indigo-600 hover:text-indigo-800">
({{ tool.scrutiny_report.findings | selectattr('result', 'equalto', 'warning') | list | length }} warnings)
</button>
<div id="findings-{{ tool.id }}" class="hidden mt-2 text-xs space-y-1 max-w-md">
{% for finding in tool.scrutiny_report.findings %}
{% if finding.result == 'warning' %}
<div class="p-2 rounded bg-yellow-50">
<span class="font-medium">{{ finding.check }}:</span>
<span class="text-yellow-700">{{ finding.message }}</span>
{% if finding.suggestion %}
<div class="text-gray-500 italic text-xs">{{ finding.suggestion }}</div>
{% endif %}
</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ tool.published_at[:10] if tool.published_at else 'Unknown' }}
@@ -100,6 +129,11 @@
<script>
let currentToolId = null;
function toggleFindings(toolId) {
const el = document.getElementById(`findings-${toolId}`);
el.classList.toggle('hidden');
}
async function approveTool(toolId) {
if (!confirm('Approve this tool?')) return;
try {
@@ -0,0 +1,179 @@
{% extends "dashboard/base.html" %}
{% block dashboard_header %}
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900">Scrutiny Audit</h1>
<p class="mt-1 text-gray-600">Review automated scrutiny results for all tools</p>
</div>
<a href="{{ url_for('web.admin_dashboard') }}" class="text-sm text-indigo-600 hover:text-indigo-700">&larr; Back to Admin</a>
</div>
{% endblock %}
{% block dashboard_content %}
<!-- Stats Summary -->
<div class="mb-6 grid grid-cols-2 md:grid-cols-4 gap-4">
<div class="bg-white rounded-lg border border-gray-200 p-4">
<div class="text-sm text-gray-500">Auto-Approved</div>
<div class="text-2xl font-semibold text-green-600">{{ stats_summary.approved_approved or 0 }}</div>
<div class="text-xs text-gray-400">Scrutiny passed</div>
</div>
<div class="bg-white rounded-lg border border-gray-200 p-4">
<div class="text-sm text-gray-500">Needs Review</div>
<div class="text-2xl font-semibold text-yellow-600">{{ stats_summary.review_pending or 0 }}</div>
<div class="text-xs text-gray-400">Has warnings</div>
</div>
<div class="bg-white rounded-lg border border-gray-200 p-4">
<div class="text-sm text-gray-500">Manually Approved</div>
<div class="text-2xl font-semibold text-blue-600">{{ stats_summary.review_approved or 0 }}</div>
<div class="text-xs text-gray-400">With warnings</div>
</div>
<div class="bg-white rounded-lg border border-gray-200 p-4">
<div class="text-sm text-gray-500">Total Pending</div>
<div class="text-2xl font-semibold text-gray-600">{{ stats_summary.total_pending or 0 }}</div>
<div class="text-xs text-gray-400">Awaiting review</div>
</div>
</div>
<!-- Filters -->
<div class="mb-4 flex flex-wrap gap-4">
<form method="get" class="flex items-center gap-4">
<select name="scrutiny_status" class="px-3 py-2 border border-gray-300 rounded-md text-sm">
<option value="">All Scrutiny Status</option>
<option value="approved" {% if scrutiny_filter == 'approved' %}selected{% endif %}>Approved</option>
<option value="pending_review" {% if scrutiny_filter == 'pending_review' %}selected{% endif %}>Pending Review (Warnings)</option>
<option value="pending" {% if scrutiny_filter == 'pending' %}selected{% endif %}>Pending</option>
</select>
<select name="moderation_status" class="px-3 py-2 border border-gray-300 rounded-md text-sm">
<option value="">All Moderation Status</option>
<option value="approved" {% if moderation_filter == 'approved' %}selected{% endif %}>Approved</option>
<option value="pending" {% if moderation_filter == 'pending' %}selected{% endif %}>Pending</option>
<option value="rejected" {% if moderation_filter == 'rejected' %}selected{% endif %}>Rejected</option>
</select>
<button type="submit" class="px-4 py-2 bg-indigo-600 text-white text-sm font-medium rounded-md hover:bg-indigo-700">Filter</button>
{% if scrutiny_filter or moderation_filter %}
<a href="{{ url_for('web.admin_scrutiny') }}" class="text-sm text-gray-500 hover:text-gray-700">Clear</a>
{% endif %}
</form>
</div>
<div class="bg-white rounded-lg border border-gray-200">
{% if tools %}
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tool</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Scrutiny</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Moderation</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Findings</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Published</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{% for tool in tools %}
<tr class="hover:bg-gray-50">
<td class="px-4 py-4">
<div class="text-sm font-medium text-gray-900">
<a href="{{ url_for('web.tool_detail', owner=tool.owner, name=tool.name) }}" class="hover:text-indigo-600">
{{ tool.owner }}/{{ tool.name }}
</a>
</div>
<div class="text-xs text-gray-500">v{{ tool.version }} - {{ tool.category or 'Uncategorized' }}</div>
</td>
<td class="px-4 py-4 whitespace-nowrap">
{% if tool.scrutiny_status == 'approved' %}
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
Passed
</span>
{% elif tool.scrutiny_status == 'pending_review' %}
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800">
Warnings
</span>
{% else %}
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800">
{{ tool.scrutiny_status or 'N/A' }}
</span>
{% endif %}
</td>
<td class="px-4 py-4 whitespace-nowrap">
{% if tool.moderation_status == 'approved' %}
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
Approved
</span>
{% elif tool.moderation_status == 'pending' %}
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800">
Pending
</span>
{% elif tool.moderation_status == 'rejected' %}
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">
Rejected
</span>
{% endif %}
</td>
<td class="px-4 py-4">
{% if tool.scrutiny_report and tool.scrutiny_report.findings %}
<button onclick="showFindings({{ tool.id }})" class="text-sm text-indigo-600 hover:text-indigo-800">
View {{ tool.scrutiny_report.findings | length }} finding(s)
</button>
<div id="findings-{{ tool.id }}" class="hidden mt-2 text-xs space-y-1">
{% for finding in tool.scrutiny_report.findings %}
<div class="p-2 rounded {% if finding.result == 'pass' %}bg-green-50{% elif finding.result == 'warning' %}bg-yellow-50{% else %}bg-red-50{% endif %}">
<span class="font-medium">{{ finding.check }}:</span>
<span class="{% if finding.result == 'pass' %}text-green-700{% elif finding.result == 'warning' %}text-yellow-700{% else %}text-red-700{% endif %}">
{{ finding.result }}
</span>
<div class="text-gray-600">{{ finding.message }}</div>
{% if finding.suggestion %}
<div class="text-gray-500 italic">Suggestion: {{ finding.suggestion }}</div>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<span class="text-xs text-gray-400">No findings</span>
{% endif %}
</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500">
{{ tool.published_at[:10] if tool.published_at else 'Unknown' }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if meta.total_pages > 1 %}
<div class="bg-gray-50 px-6 py-3 flex items-center justify-between border-t border-gray-200">
<div class="text-sm text-gray-700">
Page {{ meta.page }} of {{ meta.total_pages }} ({{ meta.total }} total)
</div>
<div class="flex space-x-2">
{% if meta.page > 1 %}
<a href="?page={{ meta.page - 1 }}{% if scrutiny_filter %}&scrutiny_status={{ scrutiny_filter }}{% endif %}{% if moderation_filter %}&moderation_status={{ moderation_filter }}{% endif %}" class="px-3 py-1 border rounded text-sm hover:bg-gray-100">Previous</a>
{% endif %}
{% if meta.page < meta.total_pages %}
<a href="?page={{ meta.page + 1 }}{% if scrutiny_filter %}&scrutiny_status={{ scrutiny_filter }}{% endif %}{% if moderation_filter %}&moderation_status={{ moderation_filter }}{% endif %}" class="px-3 py-1 border rounded text-sm hover:bg-gray-100">Next</a>
{% endif %}
</div>
</div>
{% endif %}
{% else %}
<div class="text-center py-12">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">No tools found</h3>
<p class="mt-1 text-sm text-gray-500">Try adjusting your filters.</p>
</div>
{% endif %}
</div>
<script>
function showFindings(toolId) {
const el = document.getElementById(`findings-${toolId}`);
el.classList.toggle('hidden');
}
</script>
{% endblock %}