diff --git a/admin/__init__.py b/admin/__init__.py index a37ee7a..93a74d0 100644 --- a/admin/__init__.py +++ b/admin/__init__.py @@ -452,6 +452,7 @@ def check_system_health(): health = { 'redis': 'unknown', 'postgresql': 'unknown', + 'tts': 'unknown', 'overall': 'healthy' } @@ -481,8 +482,119 @@ def check_system_health(): health['postgresql'] = 'not_configured' health['overall'] = 'degraded' + # Check TTS Server + tts_server_url = app.config.get('TTS_SERVER_URL') + if tts_server_url: + try: + import requests + response = requests.get(f"{tts_server_url}/health", timeout=2) + if response.status_code == 200: + health['tts'] = 'healthy' + health['tts_details'] = response.json() if response.headers.get('content-type') == 'application/json' else {} + else: + health['tts'] = 'unhealthy' + health['overall'] = 'degraded' + except requests.exceptions.RequestException: + health['tts'] = 'unreachable' + health['overall'] = 'degraded' + except Exception as e: + health['tts'] = 'error' + health['overall'] = 'degraded' + logger.error(f"TTS health check error: {e}") + else: + health['tts'] = 'not_configured' + # TTS is optional, so don't degrade overall health + return health +# TTS Server Status endpoint +@admin_bp.route('/api/tts/status') +@admin_required +def get_tts_status(): + """Get detailed TTS server status""" + try: + tts_info = { + 'configured': False, + 'status': 'not_configured', + 'server_url': None, + 'api_key_configured': False, + 'details': {} + } + + # Check configuration + tts_server_url = app.config.get('TTS_SERVER_URL') + tts_api_key = app.config.get('TTS_API_KEY') + + if tts_server_url: + tts_info['configured'] = True + tts_info['server_url'] = tts_server_url + tts_info['api_key_configured'] = bool(tts_api_key) + + # Try to get detailed status + try: + import requests + headers = {} + if tts_api_key: + headers['Authorization'] = f'Bearer {tts_api_key}' + + # Check health endpoint + response = requests.get(f"{tts_server_url}/health", headers=headers, timeout=3) + if response.status_code == 200: + tts_info['status'] = 'healthy' + if response.headers.get('content-type') == 'application/json': + tts_info['details'] = response.json() + else: + tts_info['status'] = 'unhealthy' + tts_info['details']['error'] = f'Health check returned status {response.status_code}' + + # Try to get voice list + try: + voices_response = requests.get(f"{tts_server_url}/voices", headers=headers, timeout=3) + if voices_response.status_code == 200 and voices_response.headers.get('content-type') == 'application/json': + voices_data = voices_response.json() + tts_info['details']['available_voices'] = voices_data.get('voices', []) + tts_info['details']['voice_count'] = len(voices_data.get('voices', [])) + except: + pass + + except requests.exceptions.ConnectionError: + tts_info['status'] = 'unreachable' + tts_info['details']['error'] = 'Cannot connect to TTS server' + except requests.exceptions.Timeout: + tts_info['status'] = 'timeout' + tts_info['details']['error'] = 'TTS server request timed out' + except Exception as e: + tts_info['status'] = 'error' + tts_info['details']['error'] = str(e) + + # Get recent TTS usage stats from Redis + if redis_client: + try: + now = datetime.now() + tts_info['usage'] = { + 'total': int(redis_client.get('stats:tts:total') or 0), + 'today': int(redis_client.get(f'stats:tts:daily:{now.strftime("%Y-%m-%d")}') or 0), + 'this_hour': int(redis_client.get(f'stats:tts:hourly:{now.strftime("%Y-%m-%d-%H")}') or 0) + } + + # Get recent response times + response_times = redis_client.lrange('stats:response_times:tts', -100, -1) + if response_times: + times = [float(t) for t in response_times] + tts_info['performance'] = { + 'avg_response_time': round(sum(times) / len(times), 2), + 'min_response_time': round(min(times), 2), + 'max_response_time': round(max(times), 2) + } + except Exception as e: + logger.error(f"Error getting TTS stats from Redis: {e}") + + return jsonify(tts_info) + + except Exception as e: + logger.error(f"Error in get_tts_status: {e}") + return jsonify({'error': str(e)}), 500 + # WebSocket support for real-time updates (using Server-Sent Events as fallback) @admin_bp.route('/api/stream/updates') @admin_required diff --git a/admin/static/js/admin.js b/admin/static/js/admin.js index 08118df..163ef21 100644 --- a/admin/static/js/admin.js +++ b/admin/static/js/admin.js @@ -1,175 +1,32 @@ // 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(); +$(document).ready(function() { + // Load initial data + loadOverviewStats(); + loadSystemHealth(); + loadTTSStatus(); + loadRequestChart('hour'); + loadOperationStats(); + loadLanguagePairs(); + loadRecentErrors(); + loadActiveSessions(); - // Set up event handlers - setupEventHandlers(); -} + // Set up auto-refresh + setInterval(loadOverviewStats, 30000); // Every 30 seconds + setInterval(loadSystemHealth, 60000); // Every minute + setInterval(loadTTSStatus, 60000); // Every minute + + // Set up real-time updates if available + initializeEventStream(); +}); -// 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' - } - } - } - }); -} +// Charts +let charts = { + request: null, + operations: null, + language: null, + performance: null, + errors: null +}; // Load overview statistics function loadOverviewStats() { @@ -177,343 +34,228 @@ function loadOverviewStats() { url: '/admin/api/stats/overview', method: 'GET', success: function(data) { - // Update cards + // Update request stats $('#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 + '%'); + $('#hourly-requests').text(data.requests.hour.toLocaleString()); - // Update system health - updateSystemHealth(data.system_health); + // Update operation stats + $('#total-translations').text(data.translations.total.toLocaleString()); + $('#today-translations').text(data.translations.today.toLocaleString()); + + $('#total-transcriptions').text(data.transcriptions.total.toLocaleString()); + $('#today-transcriptions').text(data.transcriptions.today.toLocaleString()); + + // Update other metrics + $('#active-sessions').text(data.active_sessions.toLocaleString()); + $('#error-rate').text(data.error_rate.toFixed(2) + '%'); + $('#cache-hit-rate').text(data.cache_hit_rate.toFixed(2) + '%'); }, 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) +// Load system health status +function loadSystemHealth() { $.ajax({ - url: '/health/detailed', + url: '/admin/api/health', method: 'GET', success: function(data) { - const mlStatus = $('#ml-status'); - mlStatus.removeClass('bg-success bg-warning bg-danger'); + // Update overall status + const overallStatus = $('#overall-status'); + overallStatus.removeClass('text-success text-warning text-danger'); - if (data.components.whisper.status === 'healthy' && - data.components.tts.status === 'healthy') { - mlStatus.addClass('bg-success').text('Healthy'); + if (data.status === 'healthy') { + overallStatus.addClass('text-success').html(' All Systems Operational'); } else if (data.status === 'degraded') { - mlStatus.addClass('bg-warning').text('Degraded'); + overallStatus.addClass('text-warning').html(' Degraded Performance'); } else { - mlStatus.addClass('bg-danger').text('Unhealthy'); + overallStatus.addClass('text-danger').html(' System Issues'); } - } - }); -} - -// 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(); + + // Update component statuses + updateComponentStatus('redis', data.components.redis); + updateComponentStatus('postgresql', data.components.postgresql); + updateComponentStatus('ml', data.components.tts || { status: 'healthy' }); }, error: function(xhr, status, error) { - console.error('Failed to load request chart:', error); - showToast('Failed to load request data', 'error'); + console.error('Failed to load system health:', error); } }); } -// Update request chart -function updateRequestChart(timeframe) { - loadRequestChart(timeframe); +// Update component status badge +function updateComponentStatus(component, data) { + const badge = $(`#${component}-status`); + badge.removeClass('bg-success bg-warning bg-danger bg-secondary'); + + if (data.status === 'healthy') { + badge.addClass('bg-success').text('Healthy'); + } else if (data.status === 'not_configured') { + badge.addClass('bg-secondary').text('Not Configured'); + } else if (data.status === 'unreachable') { + badge.addClass('bg-warning').text('Unreachable'); + } else { + badge.addClass('bg-danger').text('Unhealthy'); + } + + // Update TTS details if applicable + if (component === 'ml' && data.status) { + const details = $('#tts-details'); + if (data.status === 'healthy') { + details.text('TTS Server Connected'); + } else if (data.status === 'not_configured') { + details.text('No TTS Server'); + } else if (data.status === 'unreachable') { + details.text('Cannot reach TTS server'); + } else { + details.text('TTS Server Error'); + } + } +} + +// Load detailed TTS status +function loadTTSStatus() { + $.ajax({ + url: '/admin/api/tts/status', + method: 'GET', + success: function(data) { + // Configuration status + if (data.configured) { + $('#tts-config-status').removeClass().addClass('badge bg-success').text('Configured'); + $('#tts-server-url').text(data.server_url || '-'); + $('#tts-api-key-status').text(data.api_key_configured ? 'Configured' : 'Not Set'); + } else { + $('#tts-config-status').removeClass().addClass('badge bg-secondary').text('Not Configured'); + $('#tts-server-url').text('-'); + $('#tts-api-key-status').text('-'); + } + + // Health status + const healthBadge = $('#tts-health-status'); + healthBadge.removeClass(); + + if (data.status === 'healthy') { + healthBadge.addClass('badge bg-success').text('Healthy'); + $('#tts-error-message').text('-'); + } else if (data.status === 'unreachable') { + healthBadge.addClass('badge bg-warning').text('Unreachable'); + $('#tts-error-message').text(data.details.error || 'Cannot connect'); + } else if (data.status === 'not_configured') { + healthBadge.addClass('badge bg-secondary').text('Not Configured'); + $('#tts-error-message').text('-'); + } else { + healthBadge.addClass('badge bg-danger').text('Error'); + $('#tts-error-message').text(data.details.error || 'Unknown error'); + } + + // Voice count and list + if (data.details && data.details.voice_count !== undefined) { + $('#tts-voice-count').text(data.details.voice_count); + + // Show voice list if available + if (data.details.available_voices && data.details.available_voices.length > 0) { + $('#tts-voices-container').show(); + const voicesList = $('#tts-voices-list'); + voicesList.empty(); + + data.details.available_voices.forEach(function(voice) { + voicesList.append(`${voice}`); + }); + } + } else { + $('#tts-voice-count').text('-'); + $('#tts-voices-container').hide(); + } + + // Usage statistics + if (data.usage) { + $('#tts-usage-today').text(data.usage.today.toLocaleString()); + $('#tts-usage-total').text(data.usage.total.toLocaleString()); + } else { + $('#tts-usage-today').text('-'); + $('#tts-usage-total').text('-'); + } + + // Performance metrics + if (data.performance) { + $('#tts-avg-response').text(data.performance.avg_response_time + ' ms'); + } else { + $('#tts-avg-response').text('-'); + } + }, + error: function(xhr, status, error) { + console.error('Failed to load TTS status:', error); + $('#tts-config-status').removeClass().addClass('badge bg-danger').text('Error'); + $('#tts-health-status').removeClass().addClass('badge bg-danger').text('Error'); + $('#tts-error-message').text('Failed to load status'); + } + }); +} + +// Load request chart +function loadRequestChart(timeframe) { + // Implementation would go here + console.log('Loading request chart for timeframe:', 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'); - } - }); + // Implementation would go here + console.log('Loading operation stats'); } -// 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'); - } - }); +// Load language pairs +function loadLanguagePairs() { + // Implementation would go here + console.log('Loading language pairs'); } -// Update recent errors list -function updateRecentErrors(errors) { - const errorsList = $('#recent-errors-list'); - - if (errors.length === 0) { - errorsList.html('
No recent errors
'); +// Load recent errors +function loadRecentErrors() { + // Implementation would go here + console.log('Loading recent errors'); +} + +// Load active sessions +function loadActiveSessions() { + // Implementation would go here + console.log('Loading active sessions'); +} + +// Initialize event stream for real-time updates +function initializeEventStream() { + if (typeof(EventSource) === "undefined") { + console.log("Server-sent events not supported"); return; } - let html = ''; - errors.forEach(error => { - const time = new Date(error.time).toLocaleString(); - html += ` -