talk2me/admin/static/js/admin.js
Adolfo Delorenzo fa951c3141 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>
2025-06-03 18:21:56 -06:00

519 lines
16 KiB
JavaScript

// 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();
}
});