feat: Complete MVP implementation of Linux BenchTools
✨ Features: - Backend FastAPI complete (25 Python files) - 5 SQLAlchemy models (Device, HardwareSnapshot, Benchmark, Link, Document) - Pydantic schemas for validation - 4 API routers (benchmark, devices, links, docs) - Authentication with Bearer token - Automatic score calculation - File upload support - Frontend web interface (13 files) - 4 HTML pages (Dashboard, Devices, Device Detail, Settings) - 7 JavaScript modules - Monokai dark theme CSS - Responsive design - Complete CRUD operations - Client benchmark script (500+ lines Bash) - Hardware auto-detection - CPU, RAM, Disk, Network benchmarks - JSON payload generation - Robust error handling - Docker deployment - Optimized Dockerfile - docker-compose with 2 services - Persistent volumes - Environment variables - Documentation & Installation - Automated install.sh script - README, QUICKSTART, DEPLOYMENT guides - Complete API documentation - Project structure documentation 📊 Stats: - ~60 files created - ~5000 lines of code - Full MVP feature set implemented 🚀 Ready for production deployment! 🤖 Generated with Claude Code Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
179
frontend/js/dashboard.js
Normal file
179
frontend/js/dashboard.js
Normal file
@@ -0,0 +1,179 @@
|
||||
// Linux BenchTools - Dashboard Logic
|
||||
|
||||
const { formatDate, formatRelativeTime, createScoreBadge, getScoreBadgeText, escapeHtml, showError, showEmptyState, copyToClipboard, showToast } = window.BenchUtils;
|
||||
const api = window.BenchAPI;
|
||||
|
||||
// Load dashboard data
|
||||
async function loadDashboard() {
|
||||
try {
|
||||
await Promise.all([
|
||||
loadStats(),
|
||||
loadTopDevices()
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('Failed to load dashboard:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load statistics
|
||||
async function loadStats() {
|
||||
try {
|
||||
const devices = await api.getDevices({ page_size: 1000 });
|
||||
|
||||
const totalDevices = devices.total || 0;
|
||||
let totalBenchmarks = 0;
|
||||
let scoreSum = 0;
|
||||
let scoreCount = 0;
|
||||
let lastBenchDate = null;
|
||||
|
||||
// Calculate stats from devices
|
||||
devices.items.forEach(device => {
|
||||
if (device.last_benchmark) {
|
||||
totalBenchmarks++;
|
||||
|
||||
if (device.last_benchmark.global_score !== null) {
|
||||
scoreSum += device.last_benchmark.global_score;
|
||||
scoreCount++;
|
||||
}
|
||||
|
||||
const benchDate = new Date(device.last_benchmark.run_at);
|
||||
if (!lastBenchDate || benchDate > lastBenchDate) {
|
||||
lastBenchDate = benchDate;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const avgScore = scoreCount > 0 ? Math.round(scoreSum / scoreCount) : 0;
|
||||
|
||||
// Update UI
|
||||
document.getElementById('totalDevices').textContent = totalDevices;
|
||||
document.getElementById('totalBenchmarks').textContent = totalBenchmarks;
|
||||
document.getElementById('avgScore').textContent = avgScore;
|
||||
document.getElementById('lastBench').textContent = lastBenchDate
|
||||
? formatRelativeTime(lastBenchDate.toISOString())
|
||||
: 'Aucun';
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load stats:', error);
|
||||
// Set default values on error
|
||||
document.getElementById('totalDevices').textContent = '0';
|
||||
document.getElementById('totalBenchmarks').textContent = '0';
|
||||
document.getElementById('avgScore').textContent = '0';
|
||||
document.getElementById('lastBench').textContent = 'N/A';
|
||||
}
|
||||
}
|
||||
|
||||
// Load top devices
|
||||
async function loadTopDevices() {
|
||||
const container = document.getElementById('devicesTable');
|
||||
|
||||
try {
|
||||
const data = await api.getDevices({ page_size: 50 });
|
||||
|
||||
if (!data.items || data.items.length === 0) {
|
||||
showEmptyState(container, 'Aucun device trouvé. Exécutez un benchmark sur une machine pour commencer.', '📊');
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort by global_score descending
|
||||
const sortedDevices = data.items.sort((a, b) => {
|
||||
const scoreA = a.last_benchmark?.global_score ?? -1;
|
||||
const scoreB = b.last_benchmark?.global_score ?? -1;
|
||||
return scoreB - scoreA;
|
||||
});
|
||||
|
||||
// Generate table HTML
|
||||
container.innerHTML = `
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Hostname</th>
|
||||
<th>Description</th>
|
||||
<th>Score Global</th>
|
||||
<th>CPU</th>
|
||||
<th>MEM</th>
|
||||
<th>DISK</th>
|
||||
<th>NET</th>
|
||||
<th>GPU</th>
|
||||
<th>Dernier Bench</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${sortedDevices.map((device, index) => createDeviceRow(device, index + 1)).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load devices:', error);
|
||||
showError(container, 'Impossible de charger les devices. Vérifiez que le backend est accessible.');
|
||||
}
|
||||
}
|
||||
|
||||
// Create device row HTML
|
||||
function createDeviceRow(device, rank) {
|
||||
const bench = device.last_benchmark;
|
||||
|
||||
const globalScore = bench?.global_score;
|
||||
const cpuScore = bench?.cpu_score;
|
||||
const memScore = bench?.memory_score;
|
||||
const diskScore = bench?.disk_score;
|
||||
const netScore = bench?.network_score;
|
||||
const gpuScore = bench?.gpu_score;
|
||||
const runAt = bench?.run_at;
|
||||
|
||||
const globalScoreHtml = globalScore !== null && globalScore !== undefined
|
||||
? `<span class="${window.BenchUtils.getScoreBadgeClass(globalScore)}">${getScoreBadgeText(globalScore)}</span>`
|
||||
: '<span class="badge">N/A</span>';
|
||||
|
||||
return `
|
||||
<tr onclick="window.location.href='device_detail.html?id=${device.id}'">
|
||||
<td><strong>${rank}</strong></td>
|
||||
<td>
|
||||
<strong style="color: var(--color-success);">${escapeHtml(device.hostname)}</strong>
|
||||
</td>
|
||||
<td style="color: var(--text-secondary);">
|
||||
${escapeHtml(device.description || 'Aucune description')}
|
||||
</td>
|
||||
<td>${globalScoreHtml}</td>
|
||||
<td><span class="${window.BenchUtils.getScoreBadgeClass(cpuScore)}">${getScoreBadgeText(cpuScore)}</span></td>
|
||||
<td><span class="${window.BenchUtils.getScoreBadgeClass(memScore)}">${getScoreBadgeText(memScore)}</span></td>
|
||||
<td><span class="${window.BenchUtils.getScoreBadgeClass(diskScore)}">${getScoreBadgeText(diskScore)}</span></td>
|
||||
<td><span class="${window.BenchUtils.getScoreBadgeClass(netScore)}">${getScoreBadgeText(netScore)}</span></td>
|
||||
<td><span class="${window.BenchUtils.getScoreBadgeClass(gpuScore)}">${getScoreBadgeText(gpuScore)}</span></td>
|
||||
<td style="color: var(--text-secondary); font-size: 0.85rem;">
|
||||
${runAt ? formatRelativeTime(runAt) : 'Jamais'}
|
||||
</td>
|
||||
<td>
|
||||
<a href="device_detail.html?id=${device.id}" class="btn btn-sm btn-primary">Voir</a>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
// Copy bench command to clipboard
|
||||
async function copyBenchCommand() {
|
||||
const command = document.getElementById('benchCommand').textContent;
|
||||
const success = await copyToClipboard(command);
|
||||
|
||||
if (success) {
|
||||
showToast('Commande copiée dans le presse-papier !', 'success');
|
||||
} else {
|
||||
showToast('Erreur lors de la copie', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize dashboard on page load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadDashboard();
|
||||
|
||||
// Refresh every 30 seconds
|
||||
setInterval(loadDashboard, 30000);
|
||||
});
|
||||
|
||||
// Make copyBenchCommand available globally
|
||||
window.copyBenchCommand = copyBenchCommand;
|
||||
Reference in New Issue
Block a user