Add comprehensive database integration, authentication, and admin dashboard
This commit introduces major enhancements to Talk2Me: ## Database Integration - PostgreSQL support with SQLAlchemy ORM - Redis integration for caching and real-time analytics - Automated database initialization scripts - Migration support infrastructure ## User Authentication System - JWT-based API authentication - Session-based web authentication - API key authentication for programmatic access - User roles and permissions (admin/user) - Login history and session tracking - Rate limiting per user with customizable limits ## Admin Dashboard - Real-time analytics and monitoring - User management interface (create, edit, delete users) - System health monitoring - Request/error tracking - Language pair usage statistics - Performance metrics visualization ## Key Features - Dual authentication support (token + user accounts) - Graceful fallback for missing services - Non-blocking analytics middleware - Comprehensive error handling - Session management with security features ## Bug Fixes - Fixed rate limiting bypass for admin routes - Added missing email validation method - Improved error handling for missing database tables - Fixed session-based authentication for API endpoints 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
		
							
								
								
									
										192
									
								
								admin/static/css/admin.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										192
									
								
								admin/static/css/admin.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,192 @@ | ||||
| /* Admin Dashboard Styles */ | ||||
|  | ||||
| body { | ||||
|     background-color: #f8f9fa; | ||||
|     padding-top: 56px; /* For fixed navbar */ | ||||
| } | ||||
|  | ||||
| /* Cards */ | ||||
| .card { | ||||
|     border: none; | ||||
|     box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); | ||||
|     transition: transform 0.2s; | ||||
| } | ||||
|  | ||||
| .card:hover { | ||||
|     transform: translateY(-2px); | ||||
|     box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); | ||||
| } | ||||
|  | ||||
| .card-header { | ||||
|     background-color: #fff; | ||||
|     border-bottom: 1px solid #e3e6f0; | ||||
|     font-weight: 600; | ||||
| } | ||||
|  | ||||
| /* Status Badges */ | ||||
| .badge { | ||||
|     padding: 0.375rem 0.75rem; | ||||
|     font-weight: normal; | ||||
| } | ||||
|  | ||||
| .badge.bg-success { | ||||
|     background-color: #1cc88a !important; | ||||
| } | ||||
|  | ||||
| .badge.bg-warning { | ||||
|     background-color: #f6c23e !important; | ||||
|     color: #000; | ||||
| } | ||||
|  | ||||
| .badge.bg-danger { | ||||
|     background-color: #e74a3b !important; | ||||
| } | ||||
|  | ||||
| /* Charts */ | ||||
| canvas { | ||||
|     max-width: 100%; | ||||
| } | ||||
|  | ||||
| /* Tables */ | ||||
| .table { | ||||
|     font-size: 0.875rem; | ||||
| } | ||||
|  | ||||
| .table th { | ||||
|     font-weight: 600; | ||||
|     text-transform: uppercase; | ||||
|     font-size: 0.75rem; | ||||
|     color: #6c757d; | ||||
| } | ||||
|  | ||||
| /* Login Page */ | ||||
| .login-container { | ||||
|     min-height: 100vh; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
| } | ||||
|  | ||||
| /* Responsive adjustments */ | ||||
| @media (max-width: 768px) { | ||||
|     .card-body h2 { | ||||
|         font-size: 1.5rem; | ||||
|     } | ||||
|      | ||||
|     .btn-group { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|     } | ||||
|      | ||||
|     .btn-group .btn { | ||||
|         border-radius: 0.25rem !important; | ||||
|         margin: 2px 0; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /* Loading spinners */ | ||||
| .spinner-border-sm { | ||||
|     width: 1rem; | ||||
|     height: 1rem; | ||||
| } | ||||
|  | ||||
| /* Error list */ | ||||
| .error-item { | ||||
|     padding: 0.5rem; | ||||
|     border-bottom: 1px solid #dee2e6; | ||||
| } | ||||
|  | ||||
| .error-item:last-child { | ||||
|     border-bottom: none; | ||||
| } | ||||
|  | ||||
| .error-type { | ||||
|     font-weight: 600; | ||||
|     color: #e74a3b; | ||||
| } | ||||
|  | ||||
| .error-time { | ||||
|     font-size: 0.75rem; | ||||
|     color: #6c757d; | ||||
| } | ||||
|  | ||||
| /* Toast notifications */ | ||||
| .toast { | ||||
|     background-color: white; | ||||
|     box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); | ||||
| } | ||||
|  | ||||
| /* Animations */ | ||||
| @keyframes pulse { | ||||
|     0% { | ||||
|         opacity: 1; | ||||
|     } | ||||
|     50% { | ||||
|         opacity: 0.5; | ||||
|     } | ||||
|     100% { | ||||
|         opacity: 1; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .updating { | ||||
|     animation: pulse 1s infinite; | ||||
| } | ||||
|  | ||||
| /* Dark mode support */ | ||||
| @media (prefers-color-scheme: dark) { | ||||
|     body { | ||||
|         background-color: #1a1a1a; | ||||
|         color: #e0e0e0; | ||||
|     } | ||||
|      | ||||
|     .card { | ||||
|         background-color: #2a2a2a; | ||||
|         color: #e0e0e0; | ||||
|     } | ||||
|      | ||||
|     .card-header { | ||||
|         background-color: #2a2a2a; | ||||
|         border-bottom-color: #3a3a3a; | ||||
|     } | ||||
|      | ||||
|     .table { | ||||
|         color: #e0e0e0; | ||||
|     } | ||||
|      | ||||
|     .table-striped tbody tr:nth-of-type(odd) { | ||||
|         background-color: rgba(255, 255, 255, 0.05); | ||||
|     } | ||||
|      | ||||
|     .form-control { | ||||
|         background-color: #3a3a3a; | ||||
|         border-color: #4a4a4a; | ||||
|         color: #e0e0e0; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /* Performance optimization */ | ||||
| .chart-container { | ||||
|     position: relative; | ||||
|     height: 40vh; | ||||
|     width: 100%; | ||||
| } | ||||
|  | ||||
| /* Scrollbar styling */ | ||||
| ::-webkit-scrollbar { | ||||
|     width: 8px; | ||||
|     height: 8px; | ||||
| } | ||||
|  | ||||
| ::-webkit-scrollbar-track { | ||||
|     background: #f1f1f1; | ||||
| } | ||||
|  | ||||
| ::-webkit-scrollbar-thumb { | ||||
|     background: #888; | ||||
|     border-radius: 4px; | ||||
| } | ||||
|  | ||||
| ::-webkit-scrollbar-thumb:hover { | ||||
|     background: #555; | ||||
| } | ||||
							
								
								
									
										519
									
								
								admin/static/js/admin.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										519
									
								
								admin/static/js/admin.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,519 @@ | ||||
| // Admin Dashboard JavaScript | ||||
|  | ||||
| // Global variables | ||||
| let charts = {}; | ||||
| let currentTimeframe = 'minute'; | ||||
| let eventSource = null; | ||||
|  | ||||
| // Chart.js default configuration | ||||
| Chart.defaults.responsive = true; | ||||
| Chart.defaults.maintainAspectRatio = false; | ||||
|  | ||||
| // Initialize dashboard | ||||
| function initializeDashboard() { | ||||
|     // Initialize all charts | ||||
|     initializeCharts(); | ||||
|      | ||||
|     // Set up event handlers | ||||
|     setupEventHandlers(); | ||||
| } | ||||
|  | ||||
| // Initialize all charts | ||||
| function initializeCharts() { | ||||
|     // Request Volume Chart | ||||
|     const requestCtx = document.getElementById('requestChart').getContext('2d'); | ||||
|     charts.request = new Chart(requestCtx, { | ||||
|         type: 'line', | ||||
|         data: { | ||||
|             labels: [], | ||||
|             datasets: [{ | ||||
|                 label: 'Requests', | ||||
|                 data: [], | ||||
|                 borderColor: 'rgb(75, 192, 192)', | ||||
|                 backgroundColor: 'rgba(75, 192, 192, 0.1)', | ||||
|                 tension: 0.1 | ||||
|             }] | ||||
|         }, | ||||
|         options: { | ||||
|             scales: { | ||||
|                 y: { | ||||
|                     beginAtZero: true | ||||
|                 } | ||||
|             }, | ||||
|             plugins: { | ||||
|                 legend: { | ||||
|                     display: false | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     // Language Pairs Chart | ||||
|     const languageCtx = document.getElementById('languageChart').getContext('2d'); | ||||
|     charts.language = new Chart(languageCtx, { | ||||
|         type: 'doughnut', | ||||
|         data: { | ||||
|             labels: [], | ||||
|             datasets: [{ | ||||
|                 data: [], | ||||
|                 backgroundColor: [ | ||||
|                     '#FF6384', | ||||
|                     '#36A2EB', | ||||
|                     '#FFCE56', | ||||
|                     '#4BC0C0', | ||||
|                     '#9966FF', | ||||
|                     '#FF9F40' | ||||
|                 ] | ||||
|             }] | ||||
|         }, | ||||
|         options: { | ||||
|             plugins: { | ||||
|                 legend: { | ||||
|                     position: 'bottom' | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     // Operations Chart | ||||
|     const operationsCtx = document.getElementById('operationsChart').getContext('2d'); | ||||
|     charts.operations = new Chart(operationsCtx, { | ||||
|         type: 'bar', | ||||
|         data: { | ||||
|             labels: [], | ||||
|             datasets: [ | ||||
|                 { | ||||
|                     label: 'Translations', | ||||
|                     data: [], | ||||
|                     backgroundColor: 'rgba(54, 162, 235, 0.8)' | ||||
|                 }, | ||||
|                 { | ||||
|                     label: 'Transcriptions', | ||||
|                     data: [], | ||||
|                     backgroundColor: 'rgba(255, 159, 64, 0.8)' | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
|         options: { | ||||
|             scales: { | ||||
|                 y: { | ||||
|                     beginAtZero: true | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     // Response Time Chart | ||||
|     const responseCtx = document.getElementById('responseTimeChart').getContext('2d'); | ||||
|     charts.responseTime = new Chart(responseCtx, { | ||||
|         type: 'line', | ||||
|         data: { | ||||
|             labels: ['Translation', 'Transcription', 'TTS'], | ||||
|             datasets: [ | ||||
|                 { | ||||
|                     label: 'Average', | ||||
|                     data: [], | ||||
|                     borderColor: 'rgb(75, 192, 192)', | ||||
|                     backgroundColor: 'rgba(75, 192, 192, 0.2)' | ||||
|                 }, | ||||
|                 { | ||||
|                     label: 'P95', | ||||
|                     data: [], | ||||
|                     borderColor: 'rgb(255, 206, 86)', | ||||
|                     backgroundColor: 'rgba(255, 206, 86, 0.2)' | ||||
|                 }, | ||||
|                 { | ||||
|                     label: 'P99', | ||||
|                     data: [], | ||||
|                     borderColor: 'rgb(255, 99, 132)', | ||||
|                     backgroundColor: 'rgba(255, 99, 132, 0.2)' | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
|         options: { | ||||
|             scales: { | ||||
|                 y: { | ||||
|                     beginAtZero: true, | ||||
|                     title: { | ||||
|                         display: true, | ||||
|                         text: 'Response Time (ms)' | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     // Error Type Chart | ||||
|     const errorCtx = document.getElementById('errorTypeChart').getContext('2d'); | ||||
|     charts.errorType = new Chart(errorCtx, { | ||||
|         type: 'pie', | ||||
|         data: { | ||||
|             labels: [], | ||||
|             datasets: [{ | ||||
|                 data: [], | ||||
|                 backgroundColor: [ | ||||
|                     '#e74a3b', | ||||
|                     '#f6c23e', | ||||
|                     '#4e73df', | ||||
|                     '#1cc88a', | ||||
|                     '#36b9cc', | ||||
|                     '#858796' | ||||
|                 ] | ||||
|             }] | ||||
|         }, | ||||
|         options: { | ||||
|             plugins: { | ||||
|                 legend: { | ||||
|                     position: 'right' | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
| } | ||||
|  | ||||
| // Load overview statistics | ||||
| function loadOverviewStats() { | ||||
|     $.ajax({ | ||||
|         url: '/admin/api/stats/overview', | ||||
|         method: 'GET', | ||||
|         success: function(data) { | ||||
|             // Update cards | ||||
|             $('#total-requests').text(data.requests.total.toLocaleString()); | ||||
|             $('#today-requests').text(data.requests.today.toLocaleString()); | ||||
|             $('#active-sessions').text(data.active_sessions); | ||||
|             $('#error-rate').text(data.error_rate + '%'); | ||||
|             $('#cache-hit-rate').text(data.cache_hit_rate + '%'); | ||||
|              | ||||
|             // Update system health | ||||
|             updateSystemHealth(data.system_health); | ||||
|         }, | ||||
|         error: function(xhr, status, error) { | ||||
|             console.error('Failed to load overview stats:', error); | ||||
|             showToast('Failed to load overview statistics', 'error'); | ||||
|         } | ||||
|     }); | ||||
| } | ||||
|  | ||||
| // Update system health indicators | ||||
| function updateSystemHealth(health) { | ||||
|     // Redis status | ||||
|     const redisStatus = $('#redis-status'); | ||||
|     redisStatus.removeClass('bg-success bg-warning bg-danger'); | ||||
|     if (health.redis === 'healthy') { | ||||
|         redisStatus.addClass('bg-success').text('Healthy'); | ||||
|     } else if (health.redis === 'not_configured') { | ||||
|         redisStatus.addClass('bg-warning').text('Not Configured'); | ||||
|     } else { | ||||
|         redisStatus.addClass('bg-danger').text('Unhealthy'); | ||||
|     } | ||||
|      | ||||
|     // PostgreSQL status | ||||
|     const pgStatus = $('#postgresql-status'); | ||||
|     pgStatus.removeClass('bg-success bg-warning bg-danger'); | ||||
|     if (health.postgresql === 'healthy') { | ||||
|         pgStatus.addClass('bg-success').text('Healthy'); | ||||
|     } else if (health.postgresql === 'not_configured') { | ||||
|         pgStatus.addClass('bg-warning').text('Not Configured'); | ||||
|     } else { | ||||
|         pgStatus.addClass('bg-danger').text('Unhealthy'); | ||||
|     } | ||||
|      | ||||
|     // ML services status (check via main app health endpoint) | ||||
|     $.ajax({ | ||||
|         url: '/health/detailed', | ||||
|         method: 'GET', | ||||
|         success: function(data) { | ||||
|             const mlStatus = $('#ml-status'); | ||||
|             mlStatus.removeClass('bg-success bg-warning bg-danger'); | ||||
|              | ||||
|             if (data.components.whisper.status === 'healthy' &&  | ||||
|                 data.components.tts.status === 'healthy') { | ||||
|                 mlStatus.addClass('bg-success').text('Healthy'); | ||||
|             } else if (data.status === 'degraded') { | ||||
|                 mlStatus.addClass('bg-warning').text('Degraded'); | ||||
|             } else { | ||||
|                 mlStatus.addClass('bg-danger').text('Unhealthy'); | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
| } | ||||
|  | ||||
| // Load request chart data | ||||
| function loadRequestChart(timeframe) { | ||||
|     currentTimeframe = timeframe; | ||||
|      | ||||
|     // Update button states | ||||
|     $('.btn-group button').removeClass('active'); | ||||
|     $(`button[onclick="updateRequestChart('${timeframe}')"]`).addClass('active'); | ||||
|      | ||||
|     $.ajax({ | ||||
|         url: `/admin/api/stats/requests/${timeframe}`, | ||||
|         method: 'GET', | ||||
|         success: function(data) { | ||||
|             charts.request.data.labels = data.labels; | ||||
|             charts.request.data.datasets[0].data = data.data; | ||||
|             charts.request.update(); | ||||
|         }, | ||||
|         error: function(xhr, status, error) { | ||||
|             console.error('Failed to load request chart:', error); | ||||
|             showToast('Failed to load request data', 'error'); | ||||
|         } | ||||
|     }); | ||||
| } | ||||
|  | ||||
| // Update request chart | ||||
| function updateRequestChart(timeframe) { | ||||
|     loadRequestChart(timeframe); | ||||
| } | ||||
|  | ||||
| // Load operation statistics | ||||
| function loadOperationStats() { | ||||
|     $.ajax({ | ||||
|         url: '/admin/api/stats/operations', | ||||
|         method: 'GET', | ||||
|         success: function(data) { | ||||
|             // Update operations chart | ||||
|             charts.operations.data.labels = data.translations.labels; | ||||
|             charts.operations.data.datasets[0].data = data.translations.data; | ||||
|             charts.operations.data.datasets[1].data = data.transcriptions.data; | ||||
|             charts.operations.update(); | ||||
|              | ||||
|             // Update language pairs chart | ||||
|             const langPairs = Object.entries(data.language_pairs) | ||||
|                 .sort((a, b) => b[1] - a[1]) | ||||
|                 .slice(0, 6); // Top 6 language pairs | ||||
|              | ||||
|             charts.language.data.labels = langPairs.map(pair => pair[0]); | ||||
|             charts.language.data.datasets[0].data = langPairs.map(pair => pair[1]); | ||||
|             charts.language.update(); | ||||
|         }, | ||||
|         error: function(xhr, status, error) { | ||||
|             console.error('Failed to load operation stats:', error); | ||||
|             showToast('Failed to load operation data', 'error'); | ||||
|         } | ||||
|     }); | ||||
| } | ||||
|  | ||||
| // Load error statistics | ||||
| function loadErrorStats() { | ||||
|     $.ajax({ | ||||
|         url: '/admin/api/stats/errors', | ||||
|         method: 'GET', | ||||
|         success: function(data) { | ||||
|             // Update error type chart | ||||
|             const errorTypes = Object.entries(data.error_types) | ||||
|                 .sort((a, b) => b[1] - a[1]) | ||||
|                 .slice(0, 6); | ||||
|              | ||||
|             charts.errorType.data.labels = errorTypes.map(type => type[0]); | ||||
|             charts.errorType.data.datasets[0].data = errorTypes.map(type => type[1]); | ||||
|             charts.errorType.update(); | ||||
|              | ||||
|             // Update recent errors list | ||||
|             updateRecentErrors(data.recent_errors); | ||||
|         }, | ||||
|         error: function(xhr, status, error) { | ||||
|             console.error('Failed to load error stats:', error); | ||||
|             showToast('Failed to load error data', 'error'); | ||||
|         } | ||||
|     }); | ||||
| } | ||||
|  | ||||
| // Update recent errors list | ||||
| function updateRecentErrors(errors) { | ||||
|     const errorsList = $('#recent-errors-list'); | ||||
|      | ||||
|     if (errors.length === 0) { | ||||
|         errorsList.html('<p class="text-muted text-center">No recent errors</p>'); | ||||
|         return; | ||||
|     } | ||||
|      | ||||
|     let html = ''; | ||||
|     errors.forEach(error => { | ||||
|         const time = new Date(error.time).toLocaleString(); | ||||
|         html += ` | ||||
|             <div class="error-item"> | ||||
|                 <div class="error-type">${error.type}</div> | ||||
|                 <div class="text-muted small">${error.endpoint}</div> | ||||
|                 <div>${error.message}</div> | ||||
|                 <div class="error-time">${time}</div> | ||||
|             </div> | ||||
|         `; | ||||
|     }); | ||||
|      | ||||
|     errorsList.html(html); | ||||
| } | ||||
|  | ||||
| // Load performance statistics | ||||
| function loadPerformanceStats() { | ||||
|     $.ajax({ | ||||
|         url: '/admin/api/stats/performance', | ||||
|         method: 'GET', | ||||
|         success: function(data) { | ||||
|             // Update response time chart | ||||
|             const operations = ['translation', 'transcription', 'tts']; | ||||
|             const avgData = operations.map(op => data.response_times[op].avg); | ||||
|             const p95Data = operations.map(op => data.response_times[op].p95); | ||||
|             const p99Data = operations.map(op => data.response_times[op].p99); | ||||
|              | ||||
|             charts.responseTime.data.datasets[0].data = avgData; | ||||
|             charts.responseTime.data.datasets[1].data = p95Data; | ||||
|             charts.responseTime.data.datasets[2].data = p99Data; | ||||
|             charts.responseTime.update(); | ||||
|              | ||||
|             // Update performance table | ||||
|             updatePerformanceTable(data.response_times); | ||||
|         }, | ||||
|         error: function(xhr, status, error) { | ||||
|             console.error('Failed to load performance stats:', error); | ||||
|             showToast('Failed to load performance data', 'error'); | ||||
|         } | ||||
|     }); | ||||
| } | ||||
|  | ||||
| // Update performance table | ||||
| function updatePerformanceTable(responseData) { | ||||
|     const tbody = $('#performance-table'); | ||||
|     let html = ''; | ||||
|      | ||||
|     const operations = { | ||||
|         'translation': 'Translation', | ||||
|         'transcription': 'Transcription', | ||||
|         'tts': 'Text-to-Speech' | ||||
|     }; | ||||
|      | ||||
|     for (const [key, label] of Object.entries(operations)) { | ||||
|         const data = responseData[key]; | ||||
|         html += ` | ||||
|             <tr> | ||||
|                 <td>${label}</td> | ||||
|                 <td>${data.avg || '-'}</td> | ||||
|                 <td>${data.p95 || '-'}</td> | ||||
|                 <td>${data.p99 || '-'}</td> | ||||
|             </tr> | ||||
|         `; | ||||
|     } | ||||
|      | ||||
|     tbody.html(html); | ||||
| } | ||||
|  | ||||
| // Start real-time updates | ||||
| function startRealtimeUpdates() { | ||||
|     if (eventSource) { | ||||
|         eventSource.close(); | ||||
|     } | ||||
|      | ||||
|     eventSource = new EventSource('/admin/api/stream/updates'); | ||||
|      | ||||
|     eventSource.onmessage = function(event) { | ||||
|         const data = JSON.parse(event.data); | ||||
|          | ||||
|         // Update real-time metrics | ||||
|         if (data.requests_per_minute !== undefined) { | ||||
|             $('#requests-per-minute').text(data.requests_per_minute); | ||||
|         } | ||||
|          | ||||
|         if (data.active_sessions !== undefined) { | ||||
|             $('#active-sessions').text(data.active_sessions); | ||||
|         } | ||||
|          | ||||
|         // Update last update time | ||||
|         $('#last-update').text('Just now'); | ||||
|          | ||||
|         // Show update indicator | ||||
|         $('#update-status').text('Connected').removeClass('text-danger').addClass('text-success'); | ||||
|     }; | ||||
|      | ||||
|     eventSource.onerror = function(error) { | ||||
|         console.error('EventSource error:', error); | ||||
|         $('#update-status').text('Disconnected').removeClass('text-success').addClass('text-danger'); | ||||
|          | ||||
|         // Reconnect after 5 seconds | ||||
|         setTimeout(startRealtimeUpdates, 5000); | ||||
|     }; | ||||
| } | ||||
|  | ||||
| // Export data function | ||||
| function exportData(dataType) { | ||||
|     window.location.href = `/admin/api/export/${dataType}`; | ||||
| } | ||||
|  | ||||
| // Show toast notification | ||||
| function showToast(message, type = 'info') { | ||||
|     const toast = $('#update-toast'); | ||||
|     const toastBody = toast.find('.toast-body'); | ||||
|      | ||||
|     toastBody.removeClass('text-success text-danger text-warning'); | ||||
|      | ||||
|     if (type === 'success') { | ||||
|         toastBody.addClass('text-success'); | ||||
|     } else if (type === 'error') { | ||||
|         toastBody.addClass('text-danger'); | ||||
|     } else if (type === 'warning') { | ||||
|         toastBody.addClass('text-warning'); | ||||
|     } | ||||
|      | ||||
|     toastBody.text(message); | ||||
|      | ||||
|     const bsToast = new bootstrap.Toast(toast[0]); | ||||
|     bsToast.show(); | ||||
| } | ||||
|  | ||||
| // Setup event handlers | ||||
| function setupEventHandlers() { | ||||
|     // Auto-refresh toggle | ||||
|     $('#auto-refresh').on('change', function() { | ||||
|         if ($(this).prop('checked')) { | ||||
|             startAutoRefresh(); | ||||
|         } else { | ||||
|             stopAutoRefresh(); | ||||
|         } | ||||
|     }); | ||||
|      | ||||
|     // Export buttons | ||||
|     $('.export-btn').on('click', function() { | ||||
|         const dataType = $(this).data('type'); | ||||
|         exportData(dataType); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| // Auto-refresh functionality | ||||
| let refreshIntervals = {}; | ||||
|  | ||||
| function startAutoRefresh() { | ||||
|     refreshIntervals.overview = setInterval(loadOverviewStats, 10000); | ||||
|     refreshIntervals.operations = setInterval(loadOperationStats, 30000); | ||||
|     refreshIntervals.errors = setInterval(loadErrorStats, 60000); | ||||
|     refreshIntervals.performance = setInterval(loadPerformanceStats, 30000); | ||||
| } | ||||
|  | ||||
| function stopAutoRefresh() { | ||||
|     Object.values(refreshIntervals).forEach(interval => clearInterval(interval)); | ||||
|     refreshIntervals = {}; | ||||
| } | ||||
|  | ||||
| // Utility functions | ||||
| function formatBytes(bytes, decimals = 2) { | ||||
|     if (bytes === 0) return '0 Bytes'; | ||||
|      | ||||
|     const k = 1024; | ||||
|     const dm = decimals < 0 ? 0 : decimals; | ||||
|     const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; | ||||
|      | ||||
|     const i = Math.floor(Math.log(bytes) / Math.log(k)); | ||||
|      | ||||
|     return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; | ||||
| } | ||||
|  | ||||
| function formatDuration(ms) { | ||||
|     if (ms < 1000) return ms + 'ms'; | ||||
|     if (ms < 60000) return (ms / 1000).toFixed(1) + 's'; | ||||
|     return (ms / 60000).toFixed(1) + 'm'; | ||||
| } | ||||
|  | ||||
| // Initialize on page load | ||||
| $(document).ready(function() { | ||||
|     if ($('#requestChart').length > 0) { | ||||
|         initializeDashboard(); | ||||
|     } | ||||
| }); | ||||
		Reference in New Issue
	
	Block a user