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:
2025-12-07 14:46:10 +01:00
parent d55a56b91f
commit c6a8e8e83d
53 changed files with 6599 additions and 1 deletions

194
frontend/js/devices.js Normal file
View 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;