Create complete dashboard HTML template

This commit is contained in:
2025-07-14 10:57:15 -05:00
parent bea580be83
commit c9e3851fc9

View File

@ -0,0 +1,476 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Discord Voice Translator - Dashboard</title>
<!-- Bootstrap CSS -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<!-- Chart.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.min.js"></script>
<!-- Socket.IO -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.4/socket.io.js"></script>
<!-- Moment.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js"></script>
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
min-height: 100vh;
}
.main-container {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 15px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
margin: 20px;
padding: 30px;
}
.stat-card {
background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%);
border: none;
border-radius: 15px;
color: white;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
}
.stat-card.primary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
.stat-card.success { background: linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%); }
.stat-card.warning { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); }
.stat-card.info { background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); }
.activity-item {
background: #f8f9fa;
border-left: 4px solid #667eea;
border-radius: 8px;
margin-bottom: 15px;
padding: 15px;
transition: background-color 0.3s ease;
}
.activity-item:hover {
background: #e9ecef;
}
.language-badge {
font-size: 0.85em;
padding: 4px 8px;
border-radius: 12px;
font-weight: 500;
}
.chart-container {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
margin-right: 8px;
}
.status-online { background-color: #28a745; }
.status-offline { background-color: #dc3545; }
.status-processing { background-color: #ffc107; animation: pulse 2s infinite; }
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.header-title {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-weight: 700;
font-size: 2.5rem;
}
.performance-badge {
display: inline-block;
padding: 2px 6px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
}
.perf-excellent { background: #d4edda; color: #155724; }
.perf-good { background: #fff3cd; color: #856404; }
.perf-poor { background: #f8d7da; color: #721c24; }
</style>
</head>
<body>
<div class="main-container">
<!-- Header -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="header-title mb-2">🎤 Voice Translator Dashboard</h1>
<p class="text-muted mb-0">Real-time monitoring of Discord voice translation activity</p>
</div>
<div class="text-end">
<div class="status-indicator status-online"></div>
<span class="text-success fw-bold">System Online</span>
<br>
<small class="text-muted">Last updated: <span id="lastUpdate"><%= moment().format('HH:mm:ss') %></span></small>
</div>
</div>
</div>
</div>
<!-- Statistics Cards -->
<div class="row mb-4">
<div class="col-lg-3 col-md-6 mb-3">
<div class="card stat-card primary h-100">
<div class="card-body text-center">
<i class="fas fa-microphone fa-2x mb-3"></i>
<h3 class="card-title mb-1" id="totalTranscriptions"><%= data.systemStats.totalTranscriptions %></h3>
<p class="card-text">Total Transcriptions</p>
<small class="opacity-75">+<%= data.systemStats.todayTranscriptions %> today</small>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6 mb-3">
<div class="card stat-card success h-100">
<div class="card-body text-center">
<i class="fas fa-clock fa-2x mb-3"></i>
<h3 class="card-title mb-1" id="totalSpeakingTime">
<%= Math.round(data.systemStats.totalSpeakingTime / 3600 * 10) / 10 %>h
</h3>
<p class="card-text">Speaking Time</p>
<small class="opacity-75">
Avg: <%= Math.round(data.systemStats.avgTranscriptionTime) %>ms
</small>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6 mb-3">
<div class="card stat-card warning h-100">
<div class="card-body text-center">
<i class="fas fa-users fa-2x mb-3"></i>
<h3 class="card-title mb-1" id="activeConnections"><%= data.systemStats.activeConnections %></h3>
<p class="card-text">Active Sessions</p>
<small class="opacity-75">Voice channels</small>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6 mb-3">
<div class="card stat-card info h-100">
<div class="card-body text-center">
<i class="fas fa-language fa-2x mb-3"></i>
<h3 class="card-title mb-1" id="languageCount">
<%= Object.keys(data.systemStats.languageBreakdown).length %>
</h3>
<p class="card-text">Languages Detected</p>
<small class="opacity-75">Last 7 days</small>
</div>
</div>
</div>
</div>
<!-- Charts Row -->
<div class="row mb-4">
<!-- Language Distribution Chart -->
<div class="col-lg-6 mb-3">
<div class="chart-container">
<h5 class="mb-3"><i class="fas fa-chart-pie me-2"></i>Language Distribution</h5>
<canvas id="languageChart" height="250"></canvas>
</div>
</div>
<!-- Performance Metrics Chart -->
<div class="col-lg-6 mb-3">
<div class="chart-container">
<h5 class="mb-3"><i class="fas fa-chart-bar me-2"></i>Performance Metrics</h5>
<canvas id="performanceChart" height="250"></canvas>
</div>
</div>
</div>
<!-- Recent Activity and User Stats -->
<div class="row">
<!-- Recent Activity -->
<div class="col-lg-8 mb-3">
<div class="chart-container">
<h5 class="mb-3"><i class="fas fa-history me-2"></i>Recent Activity</h5>
<div id="recentActivity" style="max-height: 400px; overflow-y: auto;">
<% data.recentActivity.forEach(activity => { %>
<div class="activity-item">
<div class="d-flex justify-content-between align-items-start">
<div>
<strong><%= activity.speaker_nickname %></strong>
<span class="language-badge" style="background-color: <%= activity.languageInfo.color || '#6c757d' %>20; color: <%= activity.languageInfo.color || '#6c757d' %>;">
<%= activity.languageInfo.flag %> <%= activity.languageInfo.name %>
</span>
<div class="text-muted small mt-1">
<i class="fas fa-comments me-1"></i><%= activity.channel_name %>
<i class="fas fa-clock ms-3 me-1"></i><%= activity.timeAgo %>
<i class="fas fa-stopwatch ms-3 me-1"></i><%= Math.round(activity.duration_seconds * 10) / 10 %>s
</div>
<div class="mt-2">
<small class="text-dark">"<%= activity.transcriptPreview %>"</small>
</div>
</div>
<div class="text-end">
<span class="performance-badge <%= activity.processing_time_ms < 1000 ? 'perf-excellent' : activity.processing_time_ms < 3000 ? 'perf-good' : 'perf-poor' %>">
<%= activity.processing_time_ms %>ms
</span>
</div>
</div>
</div>
<% }); %>
</div>
</div>
</div>
<!-- Top Users -->
<div class="col-lg-4 mb-3">
<div class="chart-container">
<h5 class="mb-3"><i class="fas fa-trophy me-2"></i>Top Users (7 days)</h5>
<div style="max-height: 400px; overflow-y: auto;">
<% data.userActivity.forEach((user, index) => { %>
<div class="d-flex justify-content-between align-items-center mb-3 p-2 <%= index === 0 ? 'bg-warning bg-opacity-10 rounded' : '' %>">
<div>
<div class="fw-bold">
<% if (index === 0) { %><i class="fas fa-crown text-warning me-1"></i><% } %>
<%= user.speaker_nickname %>
</div>
<small class="text-muted">
<i class="fas fa-microphone me-1"></i><%= user.transcription_count %> recordings
<br>
<i class="fas fa-clock me-1"></i><%= Math.round(user.total_speaking_time / 60 * 10) / 10 %> min
</small>
</div>
<small class="text-muted"><%= user.last_activity_ago %></small>
</div>
<% }); %>
</div>
</div>
</div>
</div>
<!-- System Information -->
<div class="row mt-4">
<div class="col-12">
<div class="chart-container">
<h5 class="mb-3"><i class="fas fa-server me-2"></i>System Information</h5>
<div class="row">
<div class="col-md-4">
<h6>Services Status</h6>
<ul class="list-unstyled">
<li><span class="status-indicator status-online"></span>Recorder Service</li>
<li><span class="status-indicator status-online"></span>Audio Processor</li>
<li><span class="status-indicator status-online"></span>Whisper Service (GPU)</li>
<li><span class="status-indicator status-online"></span>Translation Service</li>
<li><span class="status-indicator status-online"></span>Transcriber Service</li>
</ul>
</div>
<div class="col-md-4">
<h6>Performance</h6>
<ul class="list-unstyled">
<li>Avg Transcription: <strong><%= Math.round(data.systemStats.avgTranscriptionTime) %>ms</strong></li>
<li>Avg Translation: <strong><%= Math.round(data.systemStats.avgTranslationTime) %>ms</strong></li>
<li>GPU Model: <strong>faster-whisper large-v2</strong></li>
<li>Translation: <strong>Local NLLB + Google</strong></li>
</ul>
</div>
<div class="col-md-4">
<h6>Storage</h6>
<ul class="list-unstyled">
<li>Database: <strong>PostgreSQL</strong></li>
<li>Cache: <strong>Redis</strong></li>
<li>Models: <strong>Cached locally</strong></li>
<li>Audio: <strong>Auto-cleanup</strong></li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Real-time Notification Toast -->
<div class="toast-container position-fixed top-0 end-0 p-3">
<div id="liveToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header">
<strong class="me-auto">Live Update</strong>
<small id="toastTime">now</small>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body" id="toastBody">
<!-- Toast content will be inserted here -->
</div>
</div>
</div>
<!-- Bootstrap JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/js/bootstrap.bundle.min.js"></script>
<script>
// Initialize Socket.IO
const socket = io();
// Language data for charts
const languageData = <%- JSON.stringify(data.systemStats.languageBreakdown) %>;
const performanceData = <%- JSON.stringify(data.performanceMetrics) %>;
// Initialize Charts
initializeCharts();
// Socket event handlers
socket.on('transcription-completed', function(data) {
showToast(`New transcription from ${data.languageInfo.flag} ${data.languageInfo.name}`, data.text);
updateTranscriptionCount();
});
socket.on('connection-started', function(data) {
showToast('Voice Session Started', `Recording in ${data.channelName}`);
updateActiveConnections();
});
socket.on('connection-ended', function(data) {
showToast('Voice Session Ended', 'Recording stopped');
updateActiveConnections();
});
// Functions
function initializeCharts() {
// Language Distribution Pie Chart
const languageCtx = document.getElementById('languageChart').getContext('2d');
const languageLabels = Object.values(languageData).map(lang => `${lang.flag} ${lang.name}`);
const languageValues = Object.values(languageData).map(lang => lang.count);
const languageColors = Object.values(languageData).map(lang => lang.color);
new Chart(languageCtx, {
type: 'doughnut',
data: {
labels: languageLabels,
datasets: [{
data: languageValues,
backgroundColor: languageColors,
borderWidth: 2,
borderColor: '#fff'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
// Performance Metrics Bar Chart
const performanceCtx = document.getElementById('performanceChart').getContext('2d');
const serviceNames = [...new Set(performanceData.map(metric => metric.service_name))];
const avgDurations = serviceNames.map(service => {
const serviceMetrics = performanceData.filter(metric => metric.service_name === service);
return serviceMetrics.reduce((sum, metric) => sum + parseFloat(metric.avg_duration), 0) / serviceMetrics.length;
});
new Chart(performanceCtx, {
type: 'bar',
data: {
labels: serviceNames.map(name => name.replace('-', ' ').toUpperCase()),
datasets: [{
label: 'Avg Processing Time (ms)',
data: avgDurations,
backgroundColor: 'rgba(102, 126, 234, 0.8)',
borderColor: 'rgba(102, 126, 234, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Processing Time (ms)'
}
}
},
plugins: {
legend: {
display: false
}
}
}
});
}
function showToast(title, message) {
const toastElement = document.getElementById('liveToast');
const toastBody = document.getElementById('toastBody');
const toastTime = document.getElementById('toastTime');
toastBody.innerHTML = `<strong>${title}</strong><br>${message}`;
toastTime.textContent = moment().format('HH:mm:ss');
const toast = new bootstrap.Toast(toastElement);
toast.show();
}
function updateTranscriptionCount() {
const currentCount = parseInt(document.getElementById('totalTranscriptions').textContent);
document.getElementById('totalTranscriptions').textContent = currentCount + 1;
}
function updateActiveConnections() {
// This would typically fetch from API, but for demo we'll simulate
fetch('/api/stats')
.then(response => response.json())
.then(data => {
document.getElementById('activeConnections').textContent = data.activeConnections;
})
.catch(error => console.error('Error updating stats:', error));
}
function updateLastUpdateTime() {
document.getElementById('lastUpdate').textContent = moment().format('HH:mm:ss');
}
// Update timestamp every second
setInterval(updateLastUpdateTime, 1000);
// Refresh data every 30 seconds
setInterval(function() {
location.reload();
}, 30000);
console.log('🎤 Discord Voice Translator Dashboard loaded successfully!');
</script>
</body>
</html>