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:
194
frontend/js/devices.js
Normal file
194
frontend/js/devices.js
Normal file
@@ -0,0 +1,194 @@
|
||||
// Linux BenchTools - Devices List Logic
|
||||
|
||||
const { formatRelativeTime, createScoreBadge, getScoreBadgeText, escapeHtml, showError, showEmptyState, formatTags, debounce } = window.BenchUtils;
|
||||
const api = window.BenchAPI;
|
||||
|
||||
let currentPage = 1;
|
||||
const pageSize = 20;
|
||||
let searchQuery = '';
|
||||
let allDevices = [];
|
||||
|
||||
// Load devices
|
||||
async function loadDevices() {
|
||||
const container = document.getElementById('devicesContainer');
|
||||
|
||||
try {
|
||||
const data = await api.getDevices({ page_size: 1000 }); // Get all for client-side filtering
|
||||
|
||||
allDevices = data.items || [];
|
||||
|
||||
if (allDevices.length === 0) {
|
||||
showEmptyState(container, 'Aucun device trouvé. Exécutez un benchmark sur une machine pour commencer.', '📊');
|
||||
return;
|
||||
}
|
||||
|
||||
renderDevices();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load devices:', error);
|
||||
showError(container, 'Impossible de charger les devices. Vérifiez que le backend est accessible.');
|
||||
}
|
||||
}
|
||||
|
||||
// Filter devices based on search query
|
||||
function filterDevices() {
|
||||
if (!searchQuery) {
|
||||
return allDevices;
|
||||
}
|
||||
|
||||
const query = searchQuery.toLowerCase();
|
||||
|
||||
return allDevices.filter(device => {
|
||||
const hostname = (device.hostname || '').toLowerCase();
|
||||
const description = (device.description || '').toLowerCase();
|
||||
const tags = (device.tags || '').toLowerCase();
|
||||
const location = (device.location || '').toLowerCase();
|
||||
|
||||
return hostname.includes(query) ||
|
||||
description.includes(query) ||
|
||||
tags.includes(query) ||
|
||||
location.includes(query);
|
||||
});
|
||||
}
|
||||
|
||||
// Render devices
|
||||
function renderDevices() {
|
||||
const container = document.getElementById('devicesContainer');
|
||||
const filteredDevices = filterDevices();
|
||||
|
||||
if (filteredDevices.length === 0) {
|
||||
showEmptyState(container, 'Aucun device ne correspond à votre recherche.', '🔍');
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort by global_score descending
|
||||
const sortedDevices = filteredDevices.sort((a, b) => {
|
||||
const scoreA = a.last_benchmark?.global_score ?? -1;
|
||||
const scoreB = b.last_benchmark?.global_score ?? -1;
|
||||
return scoreB - scoreA;
|
||||
});
|
||||
|
||||
// Pagination
|
||||
const startIndex = (currentPage - 1) * pageSize;
|
||||
const endIndex = startIndex + pageSize;
|
||||
const paginatedDevices = sortedDevices.slice(startIndex, endIndex);
|
||||
|
||||
// Render device cards
|
||||
container.innerHTML = paginatedDevices.map(device => createDeviceCard(device)).join('');
|
||||
|
||||
// Render pagination
|
||||
renderPagination(filteredDevices.length);
|
||||
}
|
||||
|
||||
// Create device card HTML
|
||||
function createDeviceCard(device) {
|
||||
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 `
|
||||
<div class="device-card" onclick="window.location.href='device_detail.html?id=${device.id}'">
|
||||
<div class="device-card-header">
|
||||
<div>
|
||||
<div class="device-card-title">${escapeHtml(device.hostname)}</div>
|
||||
<div style="color: var(--text-secondary); font-size: 0.9rem; margin-top: 0.25rem;">
|
||||
${escapeHtml(device.description || 'Aucune description')}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
${globalScoreHtml}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="device-card-meta">
|
||||
${device.location ? `<span>📍 ${escapeHtml(device.location)}</span>` : ''}
|
||||
${bench?.run_at ? `<span>⏱️ ${formatRelativeTime(runAt)}</span>` : ''}
|
||||
</div>
|
||||
|
||||
${device.tags ? `<div class="tags" style="margin-bottom: 1rem;">${formatTags(device.tags)}</div>` : ''}
|
||||
|
||||
<div class="device-card-scores">
|
||||
${createScoreBadge(cpuScore, 'CPU')}
|
||||
${createScoreBadge(memScore, 'MEM')}
|
||||
${createScoreBadge(diskScore, 'DISK')}
|
||||
${createScoreBadge(netScore, 'NET')}
|
||||
${createScoreBadge(gpuScore, 'GPU')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Render pagination
|
||||
function renderPagination(totalItems) {
|
||||
const container = document.getElementById('paginationContainer');
|
||||
|
||||
if (totalItems <= pageSize) {
|
||||
container.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const totalPages = Math.ceil(totalItems / pageSize);
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="pagination">
|
||||
<button
|
||||
class="pagination-btn"
|
||||
onclick="changePage(${currentPage - 1})"
|
||||
${currentPage === 1 ? 'disabled' : ''}
|
||||
>
|
||||
← Précédent
|
||||
</button>
|
||||
|
||||
<span class="pagination-info">
|
||||
Page ${currentPage} sur ${totalPages}
|
||||
</span>
|
||||
|
||||
<button
|
||||
class="pagination-btn"
|
||||
onclick="changePage(${currentPage + 1})"
|
||||
${currentPage === totalPages ? 'disabled' : ''}
|
||||
>
|
||||
Suivant →
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Change page
|
||||
function changePage(page) {
|
||||
currentPage = page;
|
||||
renderDevices();
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
// Handle search
|
||||
const handleSearch = debounce((value) => {
|
||||
searchQuery = value;
|
||||
currentPage = 1;
|
||||
renderDevices();
|
||||
}, 300);
|
||||
|
||||
// Initialize devices page
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadDevices();
|
||||
|
||||
// Setup search
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
searchInput.addEventListener('input', (e) => handleSearch(e.target.value));
|
||||
|
||||
// Refresh every 30 seconds
|
||||
setInterval(loadDevices, 30000);
|
||||
});
|
||||
|
||||
// Make changePage available globally
|
||||
window.changePage = changePage;
|
||||
Reference in New Issue
Block a user