✨ 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>
195 lines
5.5 KiB
JavaScript
195 lines
5.5 KiB
JavaScript
// 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;
|