Create complete dashboard HTML template
This commit is contained in:
476
services/dashboard/src/views/dashboard.ejs
Normal file
476
services/dashboard/src/views/dashboard.ejs
Normal 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>
|
Reference in New Issue
Block a user