// 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('

No recent errors

'); return; } let html = ''; errors.forEach(error => { const time = new Date(error.time).toLocaleString(); html += `
${error.type}
${error.endpoint}
${error.message}
${time}
`; }); 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 += ` ${label} ${data.avg || '-'} ${data.p95 || '-'} ${data.p99 || '-'} `; } 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(); } });