fix: 15 bug fixes for v3.1.0 dashboard and Track E features

Bug fixes (ordered by priority):
- BUG-14 (P1): Fix ImportError in per-user MCP endpoint
- BUG-8 (P2): Raise clear error when PUBLIC_URL missing for OAuth
- BUG-12 (P3): Validate ENCRYPTION_KEY at startup
- BUG-13 (P4): Handle stale JWT gracefully on site creation
- BUG-11 (P5): Add auth validation to WordPress health checks
- BUG-15 (P6): Fix copy button showing Python repr (dict method shadow)
- BUG-1 (P7): Dynamic version from pyproject.toml instead of hardcoded
- BUG-5 (P8): Fix server mode default (sse → streamable-http)
- BUG-9 (P9): Plugin-specific example tools in endpoint instructions
- BUG-4 (P10): Fix settings page sidebar RBAC (session type mismatch)
- BUG-6 (P11): Show real plugin descriptions instead of 'No description'
- BUG-2 (P12): Remove stale 'phase: X.3' from system info
- BUG-10 (P13): Guard timestamps against None in Recent Activity
- BUG-3 (P14): Guard timestamps against None in audit logs
- BUG-7 (P15): Styled 404 page instead of plain text

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
airano
2026-02-24 03:30:16 +03:30
parent 684c889c27
commit 80377a1a22
11 changed files with 244 additions and 30 deletions

View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 — Page Not Found | MCP Hub</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
primary: { 400: '#60a5fa', 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8' }
}
}
}
}
</script>
</head>
<body class="bg-gray-900 text-gray-100 min-h-screen flex items-center justify-center">
<div class="text-center px-6 py-16 max-w-lg">
<div class="w-20 h-20 bg-primary-500/20 rounded-2xl flex items-center justify-center mx-auto mb-8">
<svg class="w-10 h-10 text-primary-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<h1 class="text-6xl font-bold text-white mb-4">404</h1>
<p class="text-xl text-gray-400 mb-8">Page not found</p>
<p class="text-sm text-gray-500 mb-8">The page you're looking for doesn't exist or has been moved.</p>
<div class="flex items-center justify-center gap-4">
<a href="/dashboard"
class="inline-flex items-center px-5 py-2.5 text-sm font-medium text-white bg-primary-600 hover:bg-primary-500 rounded-lg transition-colors">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
</svg>
Dashboard
</a>
<a href="/health"
class="inline-flex items-center px-5 py-2.5 text-sm font-medium text-gray-300 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-lg transition-colors">
Health Check
</a>
</div>
<p class="mt-12 text-xs text-gray-600">MCP Hub</p>
</div>
</body>
</html>

View File

@@ -178,7 +178,7 @@
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/30 transition-colors">
<!-- Timestamp -->
<td class="px-6 py-4">
<span class="text-sm text-gray-700 dark:text-gray-300 font-mono">{{ log.timestamp[:19] }}</span>
<span class="text-sm text-gray-700 dark:text-gray-300 font-mono">{{ log.timestamp[:19] if log.timestamp else '-' }}</span>
</td>
<!-- Event Type -->

View File

@@ -22,7 +22,7 @@
<p class="font-medium mb-1">{{ t.your_api_key }}</p>
<div class="flex items-center gap-2">
<code class="bg-gray-100 dark:bg-gray-900 px-3 py-1 rounded text-sm flex-1 overflow-x-auto text-gray-800 dark:text-gray-200">{{ new_key }}</code>
<button onclick="copyText('{{ new_key }}')" class="text-sm text-yellow-700 dark:text-yellow-300 hover:text-yellow-600 dark:hover:text-yellow-200">{{ t.copy }}</button>
<button onclick="copyText('{{ new_key }}')" class="text-sm text-yellow-700 dark:text-yellow-300 hover:text-yellow-600 dark:hover:text-yellow-200">{{ t['copy'] }}</button>
</div>
<p class="text-xs text-yellow-600 dark:text-yellow-400 mt-1">{{ t.key_shown_once }}</p>
</div>
@@ -33,7 +33,7 @@
<p class="font-medium mb-1">{{ t.your_api_key }}</p>
<div class="flex items-center gap-2">
<code id="new-key-value" class="bg-gray-100 dark:bg-gray-900 px-3 py-1 rounded text-sm flex-1 overflow-x-auto text-gray-800 dark:text-gray-200"></code>
<button onclick="copyNewKey()" class="text-sm text-yellow-700 dark:text-yellow-300 hover:text-yellow-600 dark:hover:text-yellow-200">{{ t.copy }}</button>
<button onclick="copyNewKey()" class="text-sm text-yellow-700 dark:text-yellow-300 hover:text-yellow-600 dark:hover:text-yellow-200">{{ t['copy'] }}</button>
</div>
<p class="text-xs text-yellow-600 dark:text-yellow-400 mt-1">{{ t.key_shown_once }}</p>
</div>
@@ -104,7 +104,7 @@
<div class="relative">
<pre id="config-output" class="bg-gray-100 dark:bg-gray-900 rounded-lg p-4 text-sm text-gray-700 dark:text-gray-300 overflow-x-auto border border-gray-200 dark:border-gray-700 min-h-[120px]">Select a site and client to generate configuration...</pre>
<button onclick="copyConfig()" class="absolute top-2 {{ 'left-2' if lang == 'fa' else 'right-2' }} text-xs bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-600 dark:text-gray-300 px-3 py-1 rounded transition-colors" id="copy-config-btn">{{ t.copy }}</button>
<button onclick="copyConfig()" class="absolute top-2 {{ 'left-2' if lang == 'fa' else 'right-2' }} text-xs bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-600 dark:text-gray-300 px-3 py-1 rounded transition-colors" id="copy-config-btn">{{ t['copy'] }}</button>
</div>
{% else %}
<p class="text-gray-500 dark:text-gray-400 text-sm">{{ t.no_sites }}. <a href="/dashboard/sites/add{% if lang and lang != 'en' %}?lang={{ lang }}{% endif %}" class="text-blue-600 dark:text-blue-400 hover:text-blue-500 dark:hover:text-blue-300">{{ t.add_site }}</a></p>
@@ -185,7 +185,7 @@ function copyConfig() {
navigator.clipboard.writeText(text);
const btn = document.getElementById('copy-config-btn');
btn.textContent = '{{ t.copied }}';
setTimeout(() => btn.textContent = '{{ t.copy }}', 2000);
setTimeout(() => btn.textContent = '{{ t['copy'] }}', 2000);
}
// Load config on page load

View File

@@ -140,7 +140,7 @@
<p class="text-xs text-gray-500 dark:text-gray-400">{{ activity.project }}</p>
</div>
</div>
<span class="text-xs text-gray-400 dark:text-gray-500">{{ activity.timestamp[:19] }}</span>
<span class="text-xs text-gray-400 dark:text-gray-500">{{ activity.timestamp[:19] if activity.timestamp else '-' }}</span>
</div>
</div>
{% endfor %}

View File

@@ -188,15 +188,15 @@
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">{% if lang == 'fa' %}نوع کاربر{% else %}User Type{% endif %}</p>
<p class="text-gray-900 dark:text-white font-mono">{{ session.user_type }}</p>
<p class="text-gray-900 dark:text-white font-mono">{{ session_display.user_type }}</p>
</div>
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">{% if lang == 'fa' %}ایجاد شده در{% else %}Created At{% endif %}</p>
<p class="text-gray-900 dark:text-white font-mono text-sm">{{ session.created_at[:19] if session.created_at else '-' }}</p>
<p class="text-gray-900 dark:text-white font-mono text-sm">{{ session_display.created_at[:19] if session_display.created_at else '-' }}</p>
</div>
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">{% if lang == 'fa' %}انقضا در{% else %}Expires At{% endif %}</p>
<p class="text-gray-900 dark:text-white font-mono text-sm">{{ session.expires_at[:19] if session.expires_at else '-' }}</p>
<p class="text-gray-900 dark:text-white font-mono text-sm">{{ session_display.expires_at[:19] if session_display.expires_at else '-' }}</p>
</div>
</div>
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">