Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Medical AI Platform - Admin Dashboard</title> | |
| <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script> | |
| <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script> | |
| <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <style> | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| .animate-spin { | |
| animation: spin 1s linear infinite; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50"> | |
| <div id="root"></div> | |
| <script type="text/babel"> | |
| const { useState, useEffect } = React; | |
| // Lucide icons as SVG components | |
| const Activity = () => ( | |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> | |
| <polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline> | |
| </svg> | |
| ); | |
| const AlertCircle = () => ( | |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> | |
| <circle cx="12" cy="12" r="10"></circle> | |
| <line x1="12" y1="8" x2="12" y2="12"></line> | |
| <line x1="12" y1="16" x2="12.01" y2="16"></line> | |
| </svg> | |
| ); | |
| const BarChart3 = () => ( | |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> | |
| <path d="M3 3v18h18"></path> | |
| <path d="M18 17V9"></path> | |
| <path d="M13 17V5"></path> | |
| <path d="M8 17v-3"></path> | |
| </svg> | |
| ); | |
| const Database = () => ( | |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> | |
| <ellipse cx="12" cy="5" rx="9" ry="3"></ellipse> | |
| <path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path> | |
| <path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path> | |
| </svg> | |
| ); | |
| const Clock = () => ( | |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> | |
| <circle cx="12" cy="12" r="10"></circle> | |
| <polyline points="12 6 12 12 16 14"></polyline> | |
| </svg> | |
| ); | |
| const Shield = () => ( | |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> | |
| <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path> | |
| </svg> | |
| ); | |
| const TrendingUp = () => ( | |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> | |
| <polyline points="23 6 13.5 15.5 8.5 10.5 1 18"></polyline> | |
| <polyline points="17 6 23 6 23 12"></polyline> | |
| </svg> | |
| ); | |
| const GitBranch = () => ( | |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> | |
| <line x1="6" y1="3" x2="6" y2="15"></line> | |
| <circle cx="18" cy="6" r="3"></circle> | |
| <circle cx="6" cy="18" r="3"></circle> | |
| <path d="M18 9a9 9 0 0 1-9 9"></path> | |
| </svg> | |
| ); | |
| const AdminDashboard = () => { | |
| const [dashboardData, setDashboardData] = useState(null); | |
| const [loading, setLoading] = useState(true); | |
| const [error, setError] = useState(null); | |
| const [activeTab, setActiveTab] = useState('overview'); | |
| const [autoRefresh, setAutoRefresh] = useState(true); | |
| const fetchDashboard = async () => { | |
| try { | |
| const response = await fetch('/health/dashboard'); | |
| if (!response.ok) throw new Error('Failed to fetch dashboard data'); | |
| const data = await response.json(); | |
| setDashboardData(data); | |
| setError(null); | |
| } catch (err) { | |
| setError(err.message); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| useEffect(() => { | |
| fetchDashboard(); | |
| if (autoRefresh) { | |
| const interval = setInterval(fetchDashboard, 10000); | |
| return () => clearInterval(interval); | |
| } | |
| }, [autoRefresh]); | |
| const StatusBadge = ({ status }) => { | |
| const colors = { | |
| operational: 'bg-green-100 text-green-800', | |
| healthy: 'bg-green-100 text-green-800', | |
| degraded: 'bg-yellow-100 text-yellow-800', | |
| critical: 'bg-red-100 text-red-800', | |
| ready: 'bg-blue-100 text-blue-800', | |
| active: 'bg-blue-100 text-blue-800' | |
| }; | |
| const color = colors[status?.toLowerCase()] || 'bg-gray-100 text-gray-800'; | |
| return <span className={`px-2 py-1 rounded-full text-xs font-medium ${color}`}>{status?.toUpperCase()}</span>; | |
| }; | |
| const MetricCard = ({ title, value, subtitle, icon, status }) => { | |
| const statusColors = { | |
| good: 'border-green-200 bg-green-50', | |
| warning: 'border-yellow-200 bg-yellow-50', | |
| critical: 'border-red-200 bg-red-50' | |
| }; | |
| const borderColor = status ? statusColors[status] : 'border-gray-200 bg-white'; | |
| return ( | |
| <div className={`p-4 rounded-lg border-2 ${borderColor}`}> | |
| <div className="flex items-center justify-between mb-2"> | |
| <span className="text-sm font-medium text-gray-600">{title}</span> | |
| <div className="text-gray-400">{icon}</div> | |
| </div> | |
| <div className="text-2xl font-bold text-gray-900">{value}</div> | |
| {subtitle && <div className="text-xs text-gray-500 mt-1">{subtitle}</div>} | |
| </div> | |
| ); | |
| }; | |
| const resolveAlert = async (alertId) => { | |
| try { | |
| await fetch(`/admin/alerts/${alertId}/resolve`, { method: 'POST' }); | |
| fetchDashboard(); | |
| } catch (err) { | |
| console.error('Failed to resolve alert:', err); | |
| } | |
| }; | |
| const clearCache = async () => { | |
| if (!confirm('Clear all cache entries? This may temporarily impact performance.')) return; | |
| try { | |
| await fetch('/admin/cache/clear', { method: 'POST' }); | |
| alert('Cache cleared successfully'); | |
| fetchDashboard(); | |
| } catch (err) { | |
| alert('Failed to clear cache'); | |
| } | |
| }; | |
| if (loading) { | |
| return ( | |
| <div className="flex items-center justify-center min-h-screen"> | |
| <div className="text-center"> | |
| <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div> | |
| <p className="text-gray-600">Loading dashboard...</p> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| if (error) { | |
| return ( | |
| <div className="flex items-center justify-center min-h-screen"> | |
| <div className="text-center"> | |
| <AlertCircle /> | |
| <p className="text-gray-600 mt-4">Error: {error}</p> | |
| <button onClick={fetchDashboard} className="mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"> | |
| Retry | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| if (!dashboardData) return null; | |
| return ( | |
| <div className="min-h-screen bg-gray-50"> | |
| <div className="bg-white border-b border-gray-200"> | |
| <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4"> | |
| <div className="flex items-center justify-between"> | |
| <div> | |
| <h1 className="text-2xl font-bold text-gray-900">Medical AI Platform - Admin Dashboard</h1> | |
| <p className="text-sm text-gray-500 mt-1">Real-time monitoring and system management</p> | |
| </div> | |
| <div className="flex items-center space-x-4"> | |
| <StatusBadge status={dashboardData.status} /> | |
| <label className="flex items-center space-x-2 text-sm text-gray-600"> | |
| <input type="checkbox" checked={autoRefresh} onChange={(e) => setAutoRefresh(e.target.checked)} className="rounded" /> | |
| <span>Auto-refresh</span> | |
| </label> | |
| <button onClick={fetchDashboard} className="px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm"> | |
| Refresh Now | |
| </button> | |
| </div> | |
| </div> | |
| <div className="mt-4 flex space-x-4 border-b border-gray-200"> | |
| {['overview', 'models', 'cache', 'alerts'].map((tab) => ( | |
| <button | |
| key={tab} | |
| onClick={() => setActiveTab(tab)} | |
| className={`px-4 py-2 font-medium text-sm border-b-2 transition-colors ${ | |
| activeTab === tab ? 'border-blue-600 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700' | |
| }`} | |
| > | |
| {tab.charAt(0).toUpperCase() + tab.slice(1)} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6"> | |
| {activeTab === 'overview' && ( | |
| <div className="space-y-6"> | |
| <div> | |
| <h2 className="text-lg font-semibold text-gray-900 mb-4">System Status</h2> | |
| <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> | |
| <MetricCard title="Uptime" value={dashboardData.system.uptime_human} subtitle={`${dashboardData.system.uptime_seconds.toLocaleString()}s`} icon={<Clock />} status="good" /> | |
| <MetricCard title="Error Rate" value={`${(dashboardData.system.error_rate * 100).toFixed(2)}%`} subtitle={`Threshold: ${(dashboardData.system.error_threshold * 100).toFixed(0)}%`} icon={<AlertCircle />} status={dashboardData.system.error_rate > dashboardData.system.error_threshold ? 'critical' : 'good'} /> | |
| <MetricCard title="Total Requests" value={dashboardData.system.total_requests.toLocaleString()} icon={<Activity />} /> | |
| <MetricCard title="Active Alerts" value={dashboardData.alerts.active_count} subtitle={`${dashboardData.alerts.critical_count} critical`} icon={<AlertCircle />} status={dashboardData.alerts.critical_count > 0 ? 'critical' : dashboardData.alerts.active_count > 0 ? 'warning' : 'good'} /> | |
| </div> | |
| </div> | |
| <div> | |
| <h2 className="text-lg font-semibold text-gray-900 mb-4">Pipeline Statistics</h2> | |
| <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> | |
| <MetricCard title="Total Jobs" value={dashboardData.pipeline.total_jobs_processed.toLocaleString()} icon={<BarChart3 />} /> | |
| <MetricCard title="Completed" value={dashboardData.pipeline.completed_jobs.toLocaleString()} icon={<TrendingUp />} status="good" /> | |
| <MetricCard title="Failed" value={dashboardData.pipeline.failed_jobs.toLocaleString()} icon={<AlertCircle />} status={dashboardData.pipeline.failed_jobs > 0 ? 'warning' : 'good'} /> | |
| <MetricCard title="Success Rate" value={`${(dashboardData.pipeline.success_rate * 100).toFixed(1)}%`} icon={<Activity />} status={dashboardData.pipeline.success_rate > 0.95 ? 'good' : dashboardData.pipeline.success_rate > 0.85 ? 'warning' : 'critical'} /> | |
| </div> | |
| </div> | |
| <div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> | |
| <div className="bg-white p-6 rounded-lg border border-gray-200"> | |
| <div className="flex items-center justify-between mb-4"> | |
| <h3 className="text-lg font-semibold text-gray-900">Cache Performance</h3> | |
| <Database /> | |
| </div> | |
| <div className="space-y-3"> | |
| <div className="flex justify-between"> | |
| <span className="text-sm text-gray-600">Hit Rate</span> | |
| <span className="text-sm font-medium">{(dashboardData.cache.hit_rate * 100).toFixed(1)}%</span> | |
| </div> | |
| <div className="flex justify-between"> | |
| <span className="text-sm text-gray-600">Entries</span> | |
| <span className="text-sm font-medium">{dashboardData.cache.total_entries.toLocaleString()}</span> | |
| </div> | |
| <div className="flex justify-between"> | |
| <span className="text-sm text-gray-600">Memory Usage</span> | |
| <span className="text-sm font-medium">{dashboardData.cache.memory_usage_mb.toFixed(1)} MB</span> | |
| </div> | |
| <div className="flex justify-between"> | |
| <span className="text-sm text-gray-600">Avg Retrieval</span> | |
| <span className="text-sm font-medium">{dashboardData.cache.avg_retrieval_time_ms.toFixed(2)} ms</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="bg-white p-6 rounded-lg border border-gray-200"> | |
| <div className="flex items-center justify-between mb-4"> | |
| <h3 className="text-lg font-semibold text-gray-900">Clinical Synthesis</h3> | |
| <GitBranch /> | |
| </div> | |
| <div className="space-y-3"> | |
| <div className="flex justify-between"> | |
| <span className="text-sm text-gray-600">Total Syntheses</span> | |
| <span className="text-sm font-medium">{dashboardData.synthesis.total_syntheses.toLocaleString()}</span> | |
| </div> | |
| <div className="flex justify-between"> | |
| <span className="text-sm text-gray-600">Avg Confidence</span> | |
| <span className="text-sm font-medium">{(dashboardData.synthesis.avg_confidence * 100).toFixed(1)}%</span> | |
| </div> | |
| <div className="flex justify-between"> | |
| <span className="text-sm text-gray-600">Requiring Review</span> | |
| <span className="text-sm font-medium">{dashboardData.synthesis.requiring_review.toLocaleString()}</span> | |
| </div> | |
| <div className="flex justify-between"> | |
| <span className="text-sm text-gray-600">Avg Processing</span> | |
| <span className="text-sm font-medium">{dashboardData.synthesis.avg_processing_time_ms.toFixed(0)} ms</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="bg-white p-6 rounded-lg border border-gray-200"> | |
| <div className="flex items-center justify-between mb-4"> | |
| <h3 className="text-lg font-semibold text-gray-900">Compliance Status</h3> | |
| <Shield /> | |
| </div> | |
| <div className="grid grid-cols-2 md:grid-cols-5 gap-4"> | |
| {Object.entries(dashboardData.compliance).map(([key, value]) => ( | |
| <div key={key} className="text-center"> | |
| <div className={`text-2xl mb-1 ${value ? 'text-green-600' : 'text-red-600'}`}> | |
| {value ? '✓' : '✗'} | |
| </div> | |
| <div className="text-xs text-gray-600"> | |
| {key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())} | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {activeTab === 'models' && ( | |
| <div> | |
| <h2 className="text-lg font-semibold text-gray-900 mb-4">Model Performance</h2> | |
| <div className="bg-white rounded-lg border border-gray-200 overflow-x-auto"> | |
| <table className="min-w-full divide-y divide-gray-200"> | |
| <thead className="bg-gray-50"> | |
| <tr> | |
| <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Model ID</th> | |
| <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Version</th> | |
| <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Inferences</th> | |
| <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Avg Latency</th> | |
| <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Error Rate</th> | |
| <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Last Used</th> | |
| </tr> | |
| </thead> | |
| <tbody className="bg-white divide-y divide-gray-200"> | |
| {Object.entries(dashboardData.models.performance).map(([modelId, perf]) => ( | |
| <tr key={modelId}> | |
| <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{modelId}</td> | |
| <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{perf.version}</td> | |
| <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{perf.total_inferences.toLocaleString()}</td> | |
| <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{perf.avg_latency_ms.toFixed(1)} ms</td> | |
| <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{(perf.error_rate * 100).toFixed(2)}%</td> | |
| <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{perf.last_used}</td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| )} | |
| {activeTab === 'cache' && ( | |
| <div> | |
| <div className="flex items-center justify-between mb-4"> | |
| <h2 className="text-lg font-semibold text-gray-900">Cache Management</h2> | |
| <button onClick={clearCache} className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 text-sm"> | |
| Clear Cache | |
| </button> | |
| </div> | |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6"> | |
| <MetricCard title="Cache Entries" value={dashboardData.cache.total_entries.toLocaleString()} icon={<Database />} /> | |
| <MetricCard title="Hit Rate" value={`${(dashboardData.cache.hit_rate * 100).toFixed(1)}%`} subtitle={`${dashboardData.cache.hits} hits, ${dashboardData.cache.misses} misses`} icon={<Activity />} status={dashboardData.cache.hit_rate > 0.7 ? 'good' : dashboardData.cache.hit_rate > 0.4 ? 'warning' : 'critical'} /> | |
| <MetricCard title="Memory Usage" value={`${dashboardData.cache.memory_usage_mb.toFixed(1)} MB`} subtitle={`${dashboardData.cache.evictions} evictions`} icon={<BarChart3 />} /> | |
| </div> | |
| <div className="bg-white p-6 rounded-lg border border-gray-200"> | |
| <h3 className="text-md font-semibold text-gray-900 mb-4">Cache Performance Analysis</h3> | |
| <div className="space-y-2"> | |
| {dashboardData.cache.hit_rate < 0.5 && ( | |
| <div className="p-3 bg-yellow-50 border border-yellow-200 rounded"> | |
| <p className="text-sm text-yellow-800"> | |
| ⚠ Low cache hit rate ({(dashboardData.cache.hit_rate * 100).toFixed(1)}%). Consider increasing cache size or TTL. | |
| </p> | |
| </div> | |
| )} | |
| {dashboardData.cache.hit_rate >= 0.8 && ( | |
| <div className="p-3 bg-green-50 border border-green-200 rounded"> | |
| <p className="text-sm text-green-800"> | |
| ✓ Excellent cache hit rate ({(dashboardData.cache.hit_rate * 100).toFixed(1)}%). Cache performing optimally. | |
| </p> | |
| </div> | |
| )} | |
| <div className="p-3 bg-blue-50 border border-blue-200 rounded"> | |
| <p className="text-sm text-blue-800"> | |
| Cache efficiency: {dashboardData.cache.cache_efficiency.toFixed(1)}% | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {activeTab === 'alerts' && ( | |
| <div> | |
| <h2 className="text-lg font-semibold text-gray-900 mb-4"> | |
| Active Alerts ({dashboardData.alerts.active_count}) | |
| </h2> | |
| {dashboardData.alerts.recent.length === 0 ? ( | |
| <div className="bg-white p-12 rounded-lg border border-gray-200 text-center"> | |
| <AlertCircle /> | |
| <p className="text-gray-500 mt-4">No active alerts</p> | |
| </div> | |
| ) : ( | |
| <div className="space-y-3"> | |
| {dashboardData.alerts.recent.map((alert) => ( | |
| <div key={alert.alert_id} className={`p-4 rounded-lg border-2 ${alert.level === 'critical' ? 'border-red-200 bg-red-50' : alert.level === 'error' ? 'border-orange-200 bg-orange-50' : alert.level === 'warning' ? 'border-yellow-200 bg-yellow-50' : 'border-blue-200 bg-blue-50'}`}> | |
| <div className="flex items-start justify-between"> | |
| <div className="flex-1"> | |
| <div className="flex items-center space-x-2 mb-2"> | |
| <span className={`px-2 py-1 rounded text-xs font-medium ${alert.level === 'critical' ? 'bg-red-200 text-red-800' : alert.level === 'error' ? 'bg-orange-200 text-orange-800' : alert.level === 'warning' ? 'bg-yellow-200 text-yellow-800' : 'bg-blue-200 text-blue-800'}`}> | |
| {alert.level.toUpperCase()} | |
| </span> | |
| <span className="text-xs text-gray-500">{alert.category}</span> | |
| <span className="text-xs text-gray-400">{new Date(alert.timestamp).toLocaleString()}</span> | |
| </div> | |
| <p className="text-sm font-medium text-gray-900">{alert.message}</p> | |
| </div> | |
| {!alert.resolved && ( | |
| <button onClick={() => resolveAlert(alert.alert_id)} className="ml-4 px-3 py-1 bg-white border border-gray-300 text-gray-700 rounded hover:bg-gray-50 text-sm"> | |
| Resolve | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| ReactDOM.render(<AdminDashboard />, document.getElementById('root')); | |
| </script> | |
| </body> | |
| </html> | |