Translator-API / public /admin.html
NathMen12's picture
Update public/admin.html
99a6406 verified
Raw
History Blame Contribute Delete
16.9 kB
<!DOCTYPE html>
<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>