Spaces:
Sleeping
Sleeping
| <html lang="fr"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Admin Panel - Translator API</title> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script> | |
| <style> | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f0f2f5; min-height: 100vh; } | |
| .header { background: #1a1a2e; color: white; padding: 20px 30px; display: flex; justify-content: space-between; align-items: center; } | |
| .header h1 { font-size: 24px; } | |
| .logout-btn { background: #e94560; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; } | |
| .container { max-width: 1400px; margin: 0 auto; padding: 20px; } | |
| .tabs { display: flex; gap: 10px; margin-bottom: 20px; background: white; border-radius: 8px; padding: 5px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); } | |
| .tab { padding: 12px 24px; background: none; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 500; color: #666; transition: all 0.2s; } | |
| .tab.active { background: #1a73e8; color: white; } | |
| .tab:hover:not(.active) { background: #f0f0f0; } | |
| .tab-content { display: none; } | |
| .tab-content.active { display: block; } | |
| .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 20px; margin-bottom: 20px; } | |
| .card { background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); padding: 20px; } | |
| .card h3 { margin-bottom: 15px; color: #333; font-size: 16px; } | |
| .chart-container { height: 300px; position: relative; } | |
| .workers-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 15px; } | |
| .worker-card { background: #f8f9fa; border-radius: 8px; padding: 15px; border: 1px solid #e9ecef; } | |
| .worker-card h4 { color: #1a73e8; margin-bottom: 10px; } | |
| .metric { display: flex; justify-content: space-between; margin: 8px 0; font-size: 14px; } | |
| .metric-label { color: #666; } | |
| .metric-value { font-weight: 600; } | |
| .status { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 8px; } | |
| .status.online { background: #28a745; } | |
| .status.offline { background: #dc3545; } | |
| .logs-table { width: 100%; border-collapse: collapse; font-size: 13px; } | |
| .logs-table th, .logs-table td { padding: 10px; text-align: left; border-bottom: 1px solid #eee; } | |
| .logs-table th { background: #f8f9fa; font-weight: 600; color: #333; } | |
| .logs-table tr:hover { background: #f8f9fa; } | |
| .logs-table .text-cell { max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | |
| .loading { text-align: center; padding: 40px; color: #666; } | |
| .auth-modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; } | |
| .auth-modal.hidden { display: none; } | |
| .auth-box { background: white; padding: 30px; border-radius: 12px; width: 100%; max-width: 400px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); } | |
| .auth-box h2 { margin-bottom: 20px; color: #333; } | |
| .auth-box input { width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 16px; margin-bottom: 15px; } | |
| .auth-box button { width: 100%; padding: 12px; background: #1a73e8; color: white; border: none; border-radius: 6px; font-size: 16px; cursor: pointer; } | |
| .auth-box button:hover { background: #1557b0; } | |
| .error-msg { color: #dc3545; margin-bottom: 15px; display: none; } | |
| .refresh-info { font-size: 12px; color: #999; margin-left: 10px; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="auth-modal" id="authModal"> | |
| <div class="auth-box"> | |
| <h2>🔐 Accès Admin</h2> | |
| <div class="error-msg" id="authError"></div> | |
| <input type="password" id="adminCode" placeholder="Code d'accès admin" autocomplete="off"> | |
| <button onclick="login()">Accéder</button> | |
| </div> | |
| </div> | |
| <header class="header"> | |
| <h1>📊 Panel Admin - Translator API</h1> | |
| <button class="logout-btn" onclick="logout()">Déconnexion</button> | |
| </header> | |
| <div class="container"> | |
| <div class="tabs"> | |
| <button class="tab active" onclick="switchTab('machines')">🖥️ Machines</button> | |
| <button class="tab" onclick="switchTab('requests')">📈 Requêtes (120s)</button> | |
| <button class="tab" onclick="switchTab('logs')">📋 Logs</button> | |
| </div> | |
| <div id="machines" class="tab-content active"> | |
| <div class="grid"> | |
| <div class="card"> | |
| <h3>🖥️ Machine Centrale - CPU</h3> | |
| <div class="chart-container"><canvas id="cpuChart"></canvas></div> | |
| </div> | |
| <div class="card"> | |
| <h3>💾 Machine Centrale - RAM</h3> | |
| <div class="chart-container"><canvas id="ramChart"></canvas></div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h3>👥 Workers Connectés</h3> | |
| <div class="workers-list" id="workersList"> | |
| <div class="loading">Chargement...</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="requests" class="tab-content"> | |
| <div class="grid"> | |
| <div class="card"> | |
| <h3>📊 Requêtes par minute (120s)</h3> | |
| <div class="chart-container"><canvas id="requestsChart"></canvas></div> | |
| </div> | |
| <div class="card"> | |
| <h3>📈 Statistiques</h3> | |
| <div id="requestStats" style="padding: 10px;">Chargement...</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="logs" class="tab-content"> | |
| <div class="card"> | |
| <h3>📋 Derniers logs de traduction</h3> | |
| <table class="logs-table"> | |
| <thead> | |
| <tr> | |
| <th>Heure</th> | |
| <th>IP</th> | |
| <th>Durée (ms)</th> | |
| <th>Langues</th> | |
| <th>Entrée</th> | |
| <th>Sortie</th> | |
| </tr> | |
| </thead> | |
| <tbody id="logsBody"> | |
| <tr><td colspan="6" class="loading">Chargement...</td></tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| let adminCode = ''; | |
| let charts = {}; | |
| let refreshInterval; | |
| // Check auth on load | |
| window.onload = () => { | |
| const savedCode = sessionStorage.getItem('adminCode'); | |
| if (savedCode) { | |
| adminCode = savedCode; | |
| document.getElementById('authModal').classList.add('hidden'); | |
| initDashboard(); | |
| } | |
| }; | |
| function login() { | |
| const code = document.getElementById('adminCode').value; | |
| if (!code) return; | |
| fetch('/api/admin/metrics', { | |
| headers: { 'x-admin-code': code } | |
| }) | |
| .then(res => { | |
| if (res.ok) { | |
| adminCode = code; | |
| sessionStorage.setItem('adminCode', code); | |
| document.getElementById('authModal').classList.add('hidden'); | |
| initDashboard(); | |
| } else { | |
| document.getElementById('authError').textContent = 'Code invalide'; | |
| document.getElementById('authError').style.display = 'block'; | |
| } | |
| }) | |
| .catch(() => { | |
| document.getElementById('authError').textContent = 'Erreur de connexion'; | |
| document.getElementById('authError').style.display = 'block'; | |
| }); | |
| } | |
| function logout() { | |
| adminCode = ''; | |
| sessionStorage.removeItem('adminCode'); | |
| document.getElementById('authModal').classList.remove('hidden'); | |
| if (refreshInterval) clearInterval(refreshInterval); | |
| } | |
| function apiCall(endpoint) { | |
| return fetch(endpoint, { | |
| headers: { 'x-admin-code': adminCode } | |
| }).then(res => res.json()); | |
| } | |
| function initDashboard() { | |
| createCharts(); | |
| loadData(); | |
| refreshInterval = setInterval(loadData, 5000); | |
| } | |
| function createCharts() { | |
| // Format time labels for linear scale | |
| const timeFormat = (ts) => new Date(ts).toLocaleTimeString(); | |
| const commonOptions = { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| animation: { duration: 500, easing: 'easeOutQuart' }, | |
| interaction: { mode: 'index', intersect: false }, | |
| scales: { | |
| x: { | |
| type: 'linear', | |
| title: { display: true, text: 'Temps' }, | |
| ticks: { | |
| maxTicksLimit: 10, | |
| callback: (value) => timeFormat(value) | |
| } | |
| }, | |
| y: { beginAtZero: true, title: { display: true } } | |
| }, | |
| plugins: { legend: { display: false } } | |
| }; | |
| charts.cpu = new Chart(document.getElementById('cpuChart'), { | |
| type: 'line', | |
| data: { datasets: [{ label: 'CPU %', data: [], borderColor: '#1a73e8', backgroundColor: 'rgba(26,115,232,0.1)', fill: true, tension: 0.4, pointRadius: 0 }] }, | |
| options: { ...commonOptions, scales: { ...commonOptions.scales, y: { ...commonOptions.scales.y, max: 100, min: 0, title: { display: true, text: '% CPU' } } } } | |
| }); | |
| charts.ram = new Chart(document.getElementById('ramChart'), { | |
| type: 'line', | |
| data: { datasets: [{ label: 'RAM (MB)', data: [], borderColor: '#34a853', backgroundColor: 'rgba(52,168,83,0.1)', fill: true, tension: 0.4, pointRadius: 0 }] }, | |
| options: { ...commonOptions, scales: { ...commonOptions.scales, y: { ...commonOptions.scales.y, title: { display: true, text: 'MB' } } } } | |
| }); | |
| charts.requests = new Chart(document.getElementById('requestsChart'), { | |
| type: 'line', | |
| data: { datasets: [{ label: 'Req/min', data: [], borderColor: '#ea4335', backgroundColor: 'rgba(234,67,53,0.1)', fill: true, tension: 0.4, pointRadius: 0 }] }, | |
| options: { | |
| ...commonOptions, | |
| scales: { | |
| x: { ...commonOptions.scales.x, title: { display: true, text: 'Temps' } }, | |
| y: { ...commonOptions.scales.y, beginAtZero: true, title: { display: true, text: 'Requêtes/min' } } | |
| } | |
| } | |
| }); | |
| } | |
| async function loadData() { | |
| try { | |
| const data = await apiCall('/api/admin/metrics'); | |
| updateMachinesTab(data); | |
| updateRequestsTab(data); | |
| updateLogsTab(data); | |
| } catch (err) { | |
| console.error('Failed to load metrics', err); | |
| } | |
| } | |
| function updateMachinesTab(data) { | |
| // Update CPU chart - use timestamp for linear scale | |
| const cpuData = data.cpuHistory.map(m => ({ x: m.timestamp, y: m.cpu })).slice(-60); | |
| charts.cpu.data.datasets[0].data = cpuData; | |
| charts.cpu.update(); | |
| // Update RAM chart - convert bytes to MB | |
| const ramData = data.ramHistory.map(m => ({ x: m.timestamp, y: (m.used / 1024 / 1024).toFixed(1) })).slice(-60); | |
| charts.ram.data.datasets[0].data = ramData; | |
| charts.ram.update(); | |
| // Update workers list | |
| const workersList = document.getElementById('workersList'); | |
| if (data.workers.length === 0) { | |
| workersList.innerHTML = '<div class="loading">Aucun worker connecté</div>'; | |
| } else { | |
| workersList.innerHTML = data.workers.map(w => ` | |
| <div class="worker-card"> | |
| <h4><span class="status ${Date.now() - w.lastHeartbeat < 10000 ? 'online' : 'offline'}"></span> Worker ${w.id.substring(0,8)}</h4> | |
| <div class="metric"><span class="metric-label">CPU</span><span class="metric-value">${w.cpu.toFixed(1)}%</span></div> | |
| <div class="metric"><span class="metric-label">RAM</span><span class="metric-value">${(w.ram / 1024 / 1024).toFixed(1)} MB</span></div> | |
| <div class="metric"><span class="metric-label">Jobs actifs</span><span class="metric-value">${w.jobsActive} / ${w.maxJobs}</span></div> | |
| <div class="metric"><span class="metric-label">Dernier heartbeat</span><span class="metric-value">${new Date(w.lastHeartbeat).toLocaleTimeString()}</span></div> | |
| </div> | |
| `).join(''); | |
| } | |
| } | |
| function updateRequestsTab(data) { | |
| // Group requests by 10-second intervals | |
| const now = Date.now(); | |
| const intervals = []; | |
| for (let i = 11; i >= 0; i--) { | |
| const start = now - (i + 1) * 10000; | |
| const end = now - i * 10000; | |
| const count = data.requests.filter(r => r.timestamp >= start && r.timestamp < end).length * 6; // project to per minute | |
| intervals.push({ time: new Date(start), count }); | |
| } | |
| charts.requests.data.labels = intervals.map(i => i.time); | |
| charts.requests.data.datasets[0].data = intervals.map(i => i.count); | |
| charts.requests.update('none'); | |
| // Stats | |
| const totalLast2min = data.requests.filter(r => r.timestamp > now - 120000).length; | |
| const avgDuration = data.requests.length ? (data.requests.reduce((a, b) => a + b.duration, 0) / data.requests.length).toFixed(0) : 0; | |
| document.getElementById('requestStats').innerHTML = ` | |
| <div class="metric"><span class="metric-label">Total requêtes (2 min)</span><span class="metric-value">${totalLast2min}</span></div> | |
| <div class="metric"><span class="metric-label">Durée moyenne</span><span class="metric-value">${avgDuration} ms</span></div> | |
| <div class="metric"><span class="metric-label">Workers actifs</span><span class="metric-value">${data.workers.filter(w => w.jobsActive > 0).length}</span></div> | |
| <div class="metric"><span class="metric-label">Taux actuel</span><span class="metric-value">${data.requestRate} req/min</span></div> | |
| `; | |
| } | |
| function updateLogsTab(data) { | |
| const logs = data.requests.slice(-50).reverse(); | |
| const tbody = document.getElementById('logsBody'); | |
| if (logs.length === 0) { | |
| tbody.innerHTML = '<tr><td colspan="6" class="loading">Aucun log</td></tr>'; | |
| return; | |
| } | |
| tbody.innerHTML = logs.map(log => ` | |
| <tr> | |
| <td>${new Date(log.timestamp).toLocaleTimeString()}</td> | |
| <td>${log.ip}</td> | |
| <td>${log.duration} ms</td> | |
| <td>${log.sourceLang} → ${log.targetLang}</td> | |
| <td class="text-cell" title="${log.inputText}">${log.inputText}</td> | |
| <td class="text-cell" title="${log.outputText}">${log.outputText}</td> | |
| </tr> | |
| `).join(''); | |
| } | |
| function switchTab(tabId) { | |
| document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); | |
| document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); | |
| document.querySelector(`[onclick="switchTab('${tabId}')"]`).classList.add('active'); | |
| document.getElementById(tabId).classList.add('active'); | |
| } | |
| // Allow Enter key in auth input | |
| document.getElementById('adminCode')?.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') login(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |