This commit is contained in:
2025-12-08 05:42:52 +01:00
parent 80d8b7aa87
commit 5d483b0df5
32 changed files with 9837 additions and 579 deletions

View File

@@ -64,6 +64,16 @@
</div>
</div>
<!-- Network Details -->
<div class="card">
<div class="card-header">🌐 Informations Réseau Détaillées</div>
<div class="card-body">
<div id="networkDetails">
<div class="loading">Chargement...</div>
</div>
</div>
</div>
<!-- Tabs -->
<div class="tabs-container">
<div class="tabs">

View File

@@ -24,25 +24,29 @@
</header>
<!-- Main Content -->
<main class="container">
<!-- Search Bar -->
<div class="search-bar">
<span class="search-icon">🔍</span>
<input
type="text"
id="searchInput"
class="search-input"
placeholder="Rechercher par hostname, description ou tags..."
>
</div>
<main class="container" style="max-width: 100%; padding: 0 1rem;">
<!-- Two-Panel Layout -->
<div style="display: flex; gap: 1rem; height: calc(100vh - 200px); margin-top: 1rem;">
<!-- Left Panel: Device List (1/5 width) -->
<div style="flex: 0 0 20%; display: flex; flex-direction: column; background: var(--card-bg); border-radius: 8px; overflow: hidden;">
<div style="padding: 1rem; border-bottom: 1px solid var(--border-color); font-weight: 600; font-size: 1.1rem;">
📋 Devices
</div>
<div id="deviceList" style="flex: 1; overflow-y: auto; padding: 0.5rem;">
<div class="loading">Chargement...</div>
</div>
</div>
<!-- Devices Grid -->
<div id="devicesContainer">
<div class="loading">Chargement des devices</div>
<!-- Right Panel: Device Details (4/5 width) -->
<div style="flex: 1; background: var(--card-bg); border-radius: 8px; overflow-y: auto;">
<div id="deviceDetailsContainer" style="padding: 2rem;">
<div style="text-align: center; color: var(--text-secondary); padding: 4rem 2rem;">
<div style="font-size: 3rem; margin-bottom: 1rem;">📊</div>
<p style="font-size: 1.1rem;">Sélectionnez un device dans la liste pour afficher ses détails</p>
</div>
</div>
</div>
</div>
<!-- Pagination -->
<div id="paginationContainer"></div>
</main>
<!-- Footer -->

View File

@@ -36,6 +36,7 @@ async function loadDeviceDetail() {
renderDeviceHeader();
renderHardwareSummary();
renderLastBenchmark();
renderNetworkDetails();
await loadBenchmarkHistory();
await loadDocuments();
await loadLinks();
@@ -86,9 +87,27 @@ function renderHardwareSummary() {
return;
}
// RAM usage info
const ramTotalGB = Math.round((snapshot.ram_total_mb || 0) / 1024);
const ramUsedMB = snapshot.ram_used_mb || 0;
const ramFreeMB = snapshot.ram_free_mb || 0;
const ramSharedMB = snapshot.ram_shared_mb || 0;
let ramValue = `${ramTotalGB} GB`;
if (ramUsedMB > 0 || ramFreeMB > 0) {
const usagePercent = ramTotalGB > 0 ? Math.round((ramUsedMB / (snapshot.ram_total_mb || 1)) * 100) : 0;
ramValue = `${ramTotalGB} GB (${usagePercent}% utilisé)<br><small>Utilisée: ${Math.round(ramUsedMB / 1024)}GB • Libre: ${Math.round(ramFreeMB / 1024)}GB`;
if (ramSharedMB > 0) {
ramValue += ` • Partagée: ${Math.round(ramSharedMB / 1024)}GB`;
}
ramValue += `<br>${snapshot.ram_slots_used || '?'} / ${snapshot.ram_slots_total || '?'} slots</small>`;
} else {
ramValue += `<br><small>${snapshot.ram_slots_used || '?'} / ${snapshot.ram_slots_total || '?'} slots</small>`;
}
const hardwareItems = [
{ label: 'CPU', icon: '🔲', value: `${snapshot.cpu_model || 'N/A'}<br><small>${snapshot.cpu_cores || 0}C / ${snapshot.cpu_threads || 0}T @ ${snapshot.cpu_max_freq_ghz || snapshot.cpu_base_freq_ghz || '?'} GHz</small>` },
{ label: 'RAM', icon: '💾', value: `${Math.round((snapshot.ram_total_mb || 0) / 1024)} GB<br><small>${snapshot.ram_slots_used || '?'} / ${snapshot.ram_slots_total || '?'} slots</small>` },
{ label: 'RAM', icon: '💾', value: ramValue },
{ label: 'GPU', icon: '🎮', value: snapshot.gpu_model || snapshot.gpu_summary || 'N/A' },
{ label: 'Stockage', icon: '💿', value: snapshot.storage_summary || 'N/A' },
{ label: 'Réseau', icon: '🌐', value: snapshot.network_interfaces_json ? `${JSON.parse(snapshot.network_interfaces_json).length} interface(s)` : 'N/A' },
@@ -141,6 +160,126 @@ function renderLastBenchmark() {
`;
}
// Render network details
function renderNetworkDetails() {
const container = document.getElementById('networkDetails');
const snapshot = currentDevice.last_hardware_snapshot;
const bench = currentDevice.last_benchmark;
if (!snapshot || !snapshot.network_interfaces_json) {
container.innerHTML = '<p style="color: var(--text-muted);">Aucune information réseau disponible</p>';
return;
}
try {
const interfaces = JSON.parse(snapshot.network_interfaces_json);
if (!interfaces || interfaces.length === 0) {
container.innerHTML = '<p style="color: var(--text-muted);">Aucune interface réseau détectée</p>';
return;
}
let html = '<div style="display: grid; gap: 1rem;">';
// Interface details
interfaces.forEach(iface => {
const typeIcon = iface.type === 'ethernet' ? '🔌' : (iface.type === 'wifi' ? '📡' : '🌐');
const wol = iface.wake_on_lan;
const wolBadge = wol === true
? '<span class="badge badge-success" style="margin-left: 0.5rem;">WoL ✓</span>'
: (wol === false ? '<span class="badge badge-muted" style="margin-left: 0.5rem;">WoL ✗</span>' : '');
html += `
<div class="hardware-item" style="border: 1px solid var(--border-color); border-radius: 8px; padding: 1rem;">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 0.75rem;">
<div>
<div style="font-weight: 600; color: var(--color-primary);">${typeIcon} ${escapeHtml(iface.name)}</div>
<div style="color: var(--text-secondary); font-size: 0.9rem; margin-top: 0.25rem;">${escapeHtml(iface.type || 'unknown')}</div>
</div>
<div>${wolBadge}</div>
</div>
<div style="display: grid; gap: 0.5rem; font-size: 0.9rem;">
${iface.ip ? `<div><strong>IP:</strong> ${escapeHtml(iface.ip)}</div>` : ''}
${iface.mac ? `<div><strong>MAC:</strong> <code>${escapeHtml(iface.mac)}</code></div>` : ''}
${iface.speed_mbps ? `<div><strong>Vitesse:</strong> ${iface.speed_mbps} Mbps</div>` : ''}
${iface.driver ? `<div><strong>Driver:</strong> ${escapeHtml(iface.driver)}</div>` : ''}
</div>
</div>
`;
});
// Network benchmark results (iperf3)
if (bench && bench.network_score !== null && bench.network_score !== undefined) {
let netBenchHtml = '<div style="border: 2px solid var(--color-info); border-radius: 8px; padding: 1rem; margin-top: 1rem;">';
netBenchHtml += '<div style="font-weight: 600; color: var(--color-info); margin-bottom: 0.75rem;">📈 Résultats Benchmark Réseau (iperf3)</div>';
netBenchHtml += '<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem;">';
// Try to parse network_results_json if available
let uploadMbps = null;
let downloadMbps = null;
let pingMs = null;
if (bench.network_results_json) {
try {
const netResults = typeof bench.network_results_json === 'string'
? JSON.parse(bench.network_results_json)
: bench.network_results_json;
uploadMbps = netResults.upload_mbps;
downloadMbps = netResults.download_mbps;
pingMs = netResults.ping_ms;
} catch (e) {
console.warn('Failed to parse network_results_json:', e);
}
}
if (uploadMbps !== null && uploadMbps !== undefined) {
netBenchHtml += `
<div style="text-align: center;">
<div style="font-size: 1.5rem; font-weight: 600; color: var(--color-success);">↑ ${uploadMbps.toFixed(2)}</div>
<div style="color: var(--text-secondary); font-size: 0.85rem;">Upload Mbps</div>
</div>
`;
}
if (downloadMbps !== null && downloadMbps !== undefined) {
netBenchHtml += `
<div style="text-align: center;">
<div style="font-size: 1.5rem; font-weight: 600; color: var(--color-info);">↓ ${downloadMbps.toFixed(2)}</div>
<div style="color: var(--text-secondary); font-size: 0.85rem;">Download Mbps</div>
</div>
`;
}
if (pingMs !== null && pingMs !== undefined) {
netBenchHtml += `
<div style="text-align: center;">
<div style="font-size: 1.5rem; font-weight: 600; color: var(--color-warning);">${typeof pingMs === 'number' ? pingMs.toFixed(2) : pingMs}</div>
<div style="color: var(--text-secondary); font-size: 0.85rem;">Ping ms</div>
</div>
`;
}
netBenchHtml += `
<div style="text-align: center;">
<div style="font-size: 1.5rem; font-weight: 600; color: var(--color-primary);">${bench.network_score.toFixed(2)}</div>
<div style="color: var(--text-secondary); font-size: 0.85rem;">Score</div>
</div>
`;
netBenchHtml += '</div></div>';
html += netBenchHtml;
}
html += '</div>';
container.innerHTML = html;
} catch (error) {
console.error('Failed to parse network interfaces:', error);
container.innerHTML = '<p style="color: var(--text-danger);">Erreur lors du parsing des données réseau</p>';
}
}
// Load benchmark history
async function loadBenchmarkHistory() {
const container = document.getElementById('benchmarkHistory');

View File

@@ -1,194 +1,327 @@
// Linux BenchTools - Devices List Logic
// Linux BenchTools - Devices Two-Panel Layout
const { formatRelativeTime, createScoreBadge, getScoreBadgeText, escapeHtml, showError, showEmptyState, formatTags, debounce } = window.BenchUtils;
const { formatRelativeTime, createScoreBadge, getScoreBadgeText, escapeHtml, showError, showEmptyState, formatTags } = window.BenchUtils;
const api = window.BenchAPI;
let currentPage = 1;
const pageSize = 20;
let searchQuery = '';
let allDevices = [];
let selectedDeviceId = null;
// Load devices
async function loadDevices() {
const container = document.getElementById('devicesContainer');
const listContainer = document.getElementById('deviceList');
try {
const data = await api.getDevices({ page_size: 1000 }); // Get all for client-side filtering
const data = await api.getDevices({ page_size: 1000 }); // Get all devices
allDevices = data.items || [];
if (allDevices.length === 0) {
showEmptyState(container, 'Aucun device trouvé. Exécutez un benchmark sur une machine pour commencer.', '📊');
listContainer.innerHTML = '<div style="padding: 1rem; text-align: center; color: var(--text-secondary);">📊<br>Aucun device</div>';
return;
}
renderDevices();
// Sort by global_score descending
allDevices.sort((a, b) => {
const scoreA = a.last_benchmark?.global_score ?? -1;
const scoreB = b.last_benchmark?.global_score ?? -1;
return scoreB - scoreA;
});
renderDeviceList();
// Auto-select first device if none selected
if (!selectedDeviceId && allDevices.length > 0) {
selectDevice(allDevices[0].id);
}
} catch (error) {
console.error('Failed to load devices:', error);
showError(container, 'Impossible de charger les devices. Vérifiez que le backend est accessible.');
listContainer.innerHTML = '<div style="padding: 1rem; color: var(--color-danger);">❌ Erreur de chargement</div>';
}
}
// Filter devices based on search query
function filterDevices() {
if (!searchQuery) {
return allDevices;
}
// Render device list (left panel)
function renderDeviceList() {
const listContainer = document.getElementById('deviceList');
const query = searchQuery.toLowerCase();
listContainer.innerHTML = allDevices.map(device => {
const globalScore = device.last_benchmark?.global_score;
const isSelected = device.id === selectedDeviceId;
return allDevices.filter(device => {
const hostname = (device.hostname || '').toLowerCase();
const description = (device.description || '').toLowerCase();
const tags = (device.tags || '').toLowerCase();
const location = (device.location || '').toLowerCase();
const scoreText = globalScore !== null && globalScore !== undefined
? Math.round(globalScore)
: 'N/A';
return hostname.includes(query) ||
description.includes(query) ||
tags.includes(query) ||
location.includes(query);
});
const scoreClass = globalScore !== null && globalScore !== undefined
? window.BenchUtils.getScoreBadgeClass(globalScore)
: 'badge';
return `
<div
class="device-list-item ${isSelected ? 'selected' : ''}"
onclick="selectDevice(${device.id})"
style="
padding: 0.75rem;
margin-bottom: 0.5rem;
background: ${isSelected ? 'var(--color-primary-alpha)' : 'var(--bg-secondary)'};
border: 1px solid ${isSelected ? 'var(--color-primary)' : 'var(--border-color)'};
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
"
onmouseover="if (!this.classList.contains('selected')) this.style.background='var(--bg-hover)'"
onmouseout="if (!this.classList.contains('selected')) this.style.background='var(--bg-secondary)'"
>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.25rem;">
<div style="font-weight: 600; font-size: 0.95rem; color: var(--text-primary);">
${escapeHtml(device.hostname)}
</div>
<span class="${scoreClass}" style="font-size: 0.75rem; padding: 0.2rem 0.5rem;">
${scoreText}
</span>
</div>
${device.last_benchmark?.run_at ? `
<div style="font-size: 0.75rem; color: var(--text-secondary);">
⏱️ ${formatRelativeTime(device.last_benchmark.run_at)}
</div>
` : '<div style="font-size: 0.75rem; color: var(--color-warning);">⚠️ Pas de benchmark</div>'}
</div>
`;
}).join('');
}
// Render devices
function renderDevices() {
const container = document.getElementById('devicesContainer');
const filteredDevices = filterDevices();
// Select device and display details
async function selectDevice(deviceId) {
selectedDeviceId = deviceId;
renderDeviceList(); // Update selection in list
if (filteredDevices.length === 0) {
showEmptyState(container, 'Aucun device ne correspond à votre recherche.', '🔍');
return;
const detailsContainer = document.getElementById('deviceDetailsContainer');
detailsContainer.innerHTML = '<div class="loading">Chargement des détails...</div>';
try {
const device = await api.getDevice(deviceId);
renderDeviceDetails(device);
} catch (error) {
console.error('Failed to load device details:', error);
showError(detailsContainer, 'Impossible de charger les détails du device.');
}
// 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) {
// Render device details (right panel)
function renderDeviceDetails(device) {
const detailsContainer = document.getElementById('deviceDetailsContainer');
const snapshot = device.last_hardware_snapshot;
const bench = device.last_benchmark;
// Hardware summary
const cpuModel = snapshot?.cpu_model || 'N/A';
const cpuCores = snapshot?.cpu_cores || '?';
const cpuThreads = snapshot?.cpu_threads || '?';
const ramTotalGB = Math.round((snapshot?.ram_total_mb || 0) / 1024);
const ramUsedMB = snapshot?.ram_used_mb || 0;
const ramFreeMB = snapshot?.ram_free_mb || 0;
const ramSharedMB = snapshot?.ram_shared_mb || 0;
const gpuSummary = snapshot?.gpu_summary || 'N/A';
const storage = snapshot?.storage_summary || 'N/A';
const osName = snapshot?.os_name || 'N/A';
const kernelVersion = snapshot?.kernel_version || 'N/A';
// RAM usage calculation
let ramUsageHtml = `${ramTotalGB} GB`;
if (ramUsedMB > 0 || ramFreeMB > 0) {
const usagePercent = ramTotalGB > 0 ? Math.round((ramUsedMB / (snapshot.ram_total_mb || 1)) * 100) : 0;
ramUsageHtml = `
${ramTotalGB} GB (${usagePercent}% utilisé)<br>
<small style="color: var(--text-secondary);">
Utilisée: ${Math.round(ramUsedMB / 1024)}GB •
Libre: ${Math.round(ramFreeMB / 1024)}GB${ramSharedMB > 0 ? ` • Partagée: ${Math.round(ramSharedMB / 1024)}GB` : ''}
</small>
`;
}
// Benchmark scores
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="${window.BenchUtils.getScoreBadgeClass(globalScore)}" style="font-size: 1.5rem; padding: 0.5rem 1rem;">${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')}
// Network details
let networkHtml = '';
if (snapshot?.network_interfaces_json) {
try {
const interfaces = JSON.parse(snapshot.network_interfaces_json);
networkHtml = interfaces.map(iface => {
const typeIcon = iface.type === 'ethernet' ? '🔌' : '📡';
const wolBadge = iface.wake_on_lan === true
? '<span class="badge badge-success" style="margin-left: 0.5rem;">WoL ✓</span>'
: '<span class="badge badge-muted" style="margin-left: 0.5rem;">WoL ✗</span>';
return `
<div style="padding: 0.75rem; background: var(--bg-secondary); border-radius: 6px; margin-bottom: 0.5rem;">
<div style="font-weight: 600; margin-bottom: 0.5rem;">
${typeIcon} ${escapeHtml(iface.name)} (${iface.type})${wolBadge}
</div>
<div style="font-size: 0.9rem; color: var(--text-secondary);">
IP: ${iface.ip || 'N/A'} • MAC: ${iface.mac || 'N/A'}<br>
Vitesse: ${iface.speed_mbps ? iface.speed_mbps + ' Mbps' : 'N/A'}
${iface.driver ? ` • Driver: ${iface.driver}` : ''}
</div>
</div>
`;
}).join('');
} catch (e) {
networkHtml = '<p style="color: var(--text-secondary);">Erreur de parsing JSON</p>';
}
}
// Network benchmark results (iperf3)
let netBenchHtml = '';
if (bench?.network_results_json) {
try {
const netResults = JSON.parse(bench.network_results_json);
netBenchHtml = `
<div style="background: var(--bg-secondary); padding: 1rem; border-radius: 6px; margin-top: 1rem;">
<div style="font-weight: 600; margin-bottom: 0.75rem;">📈 Résultats Benchmark Réseau (iperf3)</div>
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; text-align: center;">
<div>
<div style="color: var(--color-success); font-size: 1.5rem; font-weight: 600;">
${netResults.upload_mbps?.toFixed(2) || 'N/A'}
</div>
<div style="font-size: 0.85rem; color: var(--text-secondary);">Upload Mbps</div>
</div>
<div>
<div style="color: var(--color-info); font-size: 1.5rem; font-weight: 600;">
${netResults.download_mbps?.toFixed(2) || 'N/A'}
</div>
<div style="font-size: 0.85rem; color: var(--text-secondary);">Download Mbps</div>
</div>
<div>
<div style="color: var(--color-warning); font-size: 1.5rem; font-weight: 600;">
${netResults.ping_ms?.toFixed(2) || 'N/A'}
</div>
<div style="font-size: 0.85rem; color: var(--text-secondary);">Ping ms</div>
</div>
<div>
<div style="color: var(--color-primary); font-size: 1.5rem; font-weight: 600;">
${netResults.score?.toFixed(2) || 'N/A'}
</div>
<div style="font-size: 0.85rem; color: var(--text-secondary);">Score</div>
</div>
</div>
</div>
`;
} catch (e) {
console.error('Error parsing network results:', e);
}
}
detailsContainer.innerHTML = `
<!-- Device Header -->
<div style="border-bottom: 2px solid var(--border-color); padding-bottom: 1.5rem; margin-bottom: 1.5rem;">
<div style="display: flex; justify-content: space-between; align-items: start;">
<div>
<h2 style="margin: 0 0 0.5rem 0; font-size: 2rem;">${escapeHtml(device.hostname)}</h2>
<p style="color: var(--text-secondary); margin: 0;">
${escapeHtml(device.description || 'Aucune description')}
</p>
${device.location ? `<p style="color: var(--text-secondary); margin: 0.25rem 0 0 0;">📍 ${escapeHtml(device.location)}</p>` : ''}
${device.tags ? `<div class="tags" style="margin-top: 0.5rem;">${formatTags(device.tags)}</div>` : ''}
</div>
<div>
${globalScoreHtml}
</div>
</div>
</div>
<div class="device-card-meta">
${device.location ? `<span>📍 ${escapeHtml(device.location)}</span>` : ''}
${bench?.run_at ? `<span>⏱️ ${formatRelativeTime(runAt)}</span>` : ''}
<!-- Benchmark Scores -->
${bench ? `
<div style="margin-bottom: 2rem;">
<h3 style="margin: 0 0 1rem 0; font-size: 1.3rem;">📊 Scores de Benchmark</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem;">
${createScoreCard(cpuScore, 'CPU', '🔧')}
${createScoreCard(memScore, 'Mémoire', '💾')}
${createScoreCard(diskScore, 'Disque', '💿')}
${createScoreCard(netScore, 'Réseau', '🌐')}
${createScoreCard(gpuScore, 'GPU', '🎮')}
</div>
<div style="margin-top: 0.75rem; color: var(--text-secondary); font-size: 0.9rem;">
⏱️ Dernier benchmark: ${bench.run_at ? formatRelativeTime(bench.run_at) : 'N/A'}
</div>
</div>
` : '<div style="padding: 2rem; background: var(--bg-secondary); border-radius: 6px; text-align: center; color: var(--color-warning); margin-bottom: 2rem;">⚠️ Aucun benchmark disponible</div>'}
${device.tags ? `<div class="tags" style="margin-bottom: 1rem;">${formatTags(device.tags)}</div>` : ''}
<!-- Hardware Summary -->
<div style="margin-bottom: 2rem;">
<h3 style="margin: 0 0 1rem 0; font-size: 1.3rem;">🖥️ Résumé Matériel</h3>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem;">
${createInfoCard('🔧 CPU', `${escapeHtml(cpuModel)}<br><small style="color: var(--text-secondary);">${cpuCores} cores / ${cpuThreads} threads</small>`)}
${createInfoCard('💾 RAM', ramUsageHtml)}
${createInfoCard('🎮 GPU', escapeHtml(gpuSummary))}
${createInfoCard('💿 Storage', escapeHtml(storage))}
${createInfoCard('🐧 OS', `${escapeHtml(osName)}<br><small style="color: var(--text-secondary);">Kernel: ${escapeHtml(kernelVersion)}</small>`)}
${createInfoCard('⏰ Créé le', new Date(device.created_at).toLocaleDateString('fr-FR'))}
</div>
</div>
<div class="device-card-scores">
${createScoreBadge(cpuScore, 'CPU')}
${createScoreBadge(memScore, 'MEM')}
${createScoreBadge(diskScore, 'DISK')}
${createScoreBadge(netScore, 'NET')}
${createScoreBadge(gpuScore, 'GPU')}
<!-- Network Details -->
${networkHtml || netBenchHtml ? `
<div style="margin-bottom: 2rem;">
<h3 style="margin: 0 0 1rem 0; font-size: 1.3rem;">🌐 Détails Réseau</h3>
${networkHtml}
${netBenchHtml}
</div>
` : ''}
<!-- Actions -->
<div style="margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid var(--border-color);">
<a href="device_detail.html?id=${device.id}" class="btn btn-primary" style="text-decoration: none; display: inline-block;">
📄 Voir la page complète
</a>
</div>
`;
}
// Create score card for display
function createScoreCard(score, label, icon) {
const scoreValue = score !== null && score !== undefined ? Math.round(score) : 'N/A';
const badgeClass = score !== null && score !== undefined
? window.BenchUtils.getScoreBadgeClass(score)
: 'badge';
return `
<div style="background: var(--bg-secondary); padding: 1rem; border-radius: 6px; text-align: center;">
<div style="font-size: 1.5rem; margin-bottom: 0.25rem;">${icon}</div>
<div style="font-size: 0.9rem; color: var(--text-secondary); margin-bottom: 0.5rem;">${label}</div>
<div class="${badgeClass}" style="font-size: 1.25rem; padding: 0.25rem 0.75rem; display: inline-block;">
${scoreValue}
</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>
// Create info card
function createInfoCard(label, value) {
return `
<div style="background: var(--bg-secondary); padding: 1rem; border-radius: 6px;">
<div style="font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);">${label}</div>
<div style="color: var(--text-secondary);">${value}</div>
</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;
// Make selectDevice available globally
window.selectDevice = selectDevice;