Add admin configuration system and M3 content automation
Admin Configuration System: - Add registry_settings table to database schema - Create settings.py module with 17 configurable settings across 5 categories (vetting, similarity, sync, moderation, rate_limits) - Add admin API endpoints for settings management (list/get/set/reset) - Create admin settings web UI page with tabbed categories - Add CLI commands: cmdforge registry config list/get/set - Wire settings page link into admin index M3 Content Automation Scripts: - scrutiny.py: Quality scoring with 5 weighted checks - similarity.py: TF-IDF based duplicate detection - vet_pipeline.py: Combined vetting with auto-decision logic - fabric_sync.py: Scheduled Fabric pattern synchronization - fabric-sync.service/timer: systemd units for scheduled sync Settings available: - Vetting thresholds (approve/reject) and check weights - Similarity detection thresholds (duplicate/similar/related) - Fabric sync configuration (enabled, interval, auto-approve) - Moderation settings (require review, auto-approve private) - Rate limiting configuration Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
@@ -83,6 +83,15 @@
|
||||
</svg>
|
||||
<span class="text-sm font-medium text-gray-700">Manage publishers</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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
</svg>
|
||||
<span class="text-sm font-medium text-gray-700">Registry settings</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('web.dashboard') }}" 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="M11 15l-3-3m0 0l3-3m-3 3h8M3 12a9 9 0 1118 0 9 9 0 01-18 0z"/>
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
{% extends "dashboard/base.html" %}
|
||||
|
||||
{% block dashboard_header %}
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">Registry Settings</h1>
|
||||
<p class="mt-1 text-gray-600">Configure vetting, moderation, and sync settings</p>
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<button onclick="resetAllSettings()" class="px-4 py-2 text-sm font-medium text-red-700 bg-red-100 rounded-lg hover:bg-red-200">
|
||||
Reset All to Defaults
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block dashboard_content %}
|
||||
<div class="space-y-6">
|
||||
<!-- Category Tabs -->
|
||||
<div class="border-b border-gray-200">
|
||||
<nav class="-mb-px flex space-x-8" aria-label="Settings categories">
|
||||
{% for cat in categories %}
|
||||
<button onclick="showCategory('{{ cat }}')"
|
||||
id="tab-{{ cat }}"
|
||||
class="category-tab {% if loop.first %}border-indigo-500 text-indigo-600{% else %}border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300{% endif %} whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm capitalize">
|
||||
{{ cat.replace('_', ' ') }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Settings Panels -->
|
||||
{% for cat, cat_settings in settings_by_category.items() %}
|
||||
<div id="panel-{{ cat }}" class="category-panel {% if not loop.first %}hidden{% endif %}">
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
|
||||
<h3 class="text-lg font-medium text-gray-900 capitalize">{{ cat.replace('_', ' ') }} Settings</h3>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-200">
|
||||
{% for setting in cat_settings %}
|
||||
<div class="px-6 py-4 flex items-center justify-between">
|
||||
<div class="flex-1 min-w-0 pr-4">
|
||||
<p class="text-sm font-medium text-gray-900">{{ setting.key.split('.')[-1].replace('_', ' ').title() }}</p>
|
||||
<p class="text-sm text-gray-500">{{ setting.description }}</p>
|
||||
<p class="text-xs text-gray-400 mt-1">
|
||||
Key: <code class="bg-gray-100 px-1 rounded">{{ setting.key }}</code>
|
||||
{% if setting.updated_at %}
|
||||
<span class="ml-2">Last updated: {{ setting.updated_at[:10] }} by {{ setting.updated_by or 'system' }}</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
{% if setting.value_type == 'bool' %}
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox"
|
||||
id="setting-{{ setting.key|replace('.', '-') }}"
|
||||
{% if setting.value %}checked{% endif %}
|
||||
onchange="updateSetting('{{ setting.key }}', this.checked)"
|
||||
class="sr-only peer">
|
||||
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-indigo-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-indigo-600"></div>
|
||||
</label>
|
||||
{% elif setting.value_type == 'float' %}
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="range"
|
||||
id="range-{{ setting.key|replace('.', '-') }}"
|
||||
min="0" max="1" step="0.05"
|
||||
value="{{ setting.value }}"
|
||||
oninput="document.getElementById('input-{{ setting.key|replace('.', '-') }}').value = this.value"
|
||||
onchange="updateSetting('{{ setting.key }}', parseFloat(this.value))"
|
||||
class="w-24 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
|
||||
<input type="number"
|
||||
id="input-{{ setting.key|replace('.', '-') }}"
|
||||
min="0" max="1" step="0.01"
|
||||
value="{{ setting.value }}"
|
||||
onchange="updateSetting('{{ setting.key }}', parseFloat(this.value)); document.getElementById('range-{{ setting.key|replace('.', '-') }}').value = this.value"
|
||||
class="w-20 px-2 py-1 text-sm border border-gray-300 rounded-md focus:ring-indigo-500 focus:border-indigo-500">
|
||||
</div>
|
||||
{% elif setting.value_type == 'int' %}
|
||||
<input type="number"
|
||||
id="input-{{ setting.key|replace('.', '-') }}"
|
||||
value="{{ setting.value }}"
|
||||
onchange="updateSetting('{{ setting.key }}', parseInt(this.value))"
|
||||
class="w-24 px-3 py-1 text-sm border border-gray-300 rounded-md focus:ring-indigo-500 focus:border-indigo-500">
|
||||
{% else %}
|
||||
<input type="text"
|
||||
id="input-{{ setting.key|replace('.', '-') }}"
|
||||
value="{{ setting.value }}"
|
||||
onchange="updateSetting('{{ setting.key }}', this.value)"
|
||||
class="w-48 px-3 py-1 text-sm border border-gray-300 rounded-md focus:ring-indigo-500 focus:border-indigo-500">
|
||||
{% endif %}
|
||||
<button onclick="resetSetting('{{ setting.key }}')"
|
||||
title="Reset to default"
|
||||
class="p-1 text-gray-400 hover:text-gray-600">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Info Panel -->
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-blue-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-blue-800">About these settings</h3>
|
||||
<div class="mt-2 text-sm text-blue-700">
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
<li><strong>Vetting:</strong> Controls how tools are automatically scored and approved/rejected</li>
|
||||
<li><strong>Similarity:</strong> Thresholds for detecting duplicate or similar tools</li>
|
||||
<li><strong>Sync:</strong> Fabric pattern synchronization settings</li>
|
||||
<li><strong>Moderation:</strong> Rules for tool review workflow</li>
|
||||
<li><strong>Rate Limits:</strong> API rate limiting configuration</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const token = '{{ token }}';
|
||||
|
||||
function showCategory(cat) {
|
||||
// Hide all panels
|
||||
document.querySelectorAll('.category-panel').forEach(p => p.classList.add('hidden'));
|
||||
// Show selected panel
|
||||
document.getElementById('panel-' + cat).classList.remove('hidden');
|
||||
|
||||
// Update tab styles
|
||||
document.querySelectorAll('.category-tab').forEach(t => {
|
||||
t.classList.remove('border-indigo-500', 'text-indigo-600');
|
||||
t.classList.add('border-transparent', 'text-gray-500');
|
||||
});
|
||||
document.getElementById('tab-' + cat).classList.remove('border-transparent', 'text-gray-500');
|
||||
document.getElementById('tab-' + cat).classList.add('border-indigo-500', 'text-indigo-600');
|
||||
}
|
||||
|
||||
async function updateSetting(key, value) {
|
||||
try {
|
||||
const response = await fetch('/api/v1/admin/settings/' + key, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + token
|
||||
},
|
||||
body: JSON.stringify({ value: value })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showToast('Setting updated successfully', 'success');
|
||||
} else {
|
||||
showToast(data.error || 'Failed to update setting', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showToast('Network error: ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function resetSetting(key) {
|
||||
if (!confirm('Reset this setting to its default value?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v1/admin/settings/' + key, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showToast('Setting reset to default', 'success');
|
||||
location.reload();
|
||||
} else {
|
||||
showToast(data.error || 'Failed to reset setting', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showToast('Network error: ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function resetAllSettings() {
|
||||
if (!confirm('Reset ALL settings to their default values? This cannot be undone.')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v1/admin/settings/reset-all', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showToast('All settings reset to defaults', 'success');
|
||||
location.reload();
|
||||
} else {
|
||||
showToast(data.error || 'Failed to reset settings', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showToast('Network error: ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function showToast(message, type) {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg text-white ${type === 'success' ? 'bg-green-600' : 'bg-red-600'} transition-opacity duration-300`;
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = '0';
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user