328 lines
13 KiB
JavaScript
328 lines
13 KiB
JavaScript
// Linux BenchTools - Devices Two-Panel Layout
|
|
|
|
const { formatRelativeTime, createScoreBadge, getScoreBadgeText, escapeHtml, showError, showEmptyState, formatTags } = window.BenchUtils;
|
|
const api = window.BenchAPI;
|
|
|
|
let allDevices = [];
|
|
let selectedDeviceId = null;
|
|
|
|
// Load devices
|
|
async function loadDevices() {
|
|
const listContainer = document.getElementById('deviceList');
|
|
|
|
try {
|
|
const data = await api.getDevices({ page_size: 1000 }); // Get all devices
|
|
|
|
allDevices = data.items || [];
|
|
|
|
if (allDevices.length === 0) {
|
|
listContainer.innerHTML = '<div style="padding: 1rem; text-align: center; color: var(--text-secondary);">📊<br>Aucun device</div>';
|
|
return;
|
|
}
|
|
|
|
// 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);
|
|
listContainer.innerHTML = '<div style="padding: 1rem; color: var(--color-danger);">❌ Erreur de chargement</div>';
|
|
}
|
|
}
|
|
|
|
// Render device list (left panel)
|
|
function renderDeviceList() {
|
|
const listContainer = document.getElementById('deviceList');
|
|
|
|
listContainer.innerHTML = allDevices.map(device => {
|
|
const globalScore = device.last_benchmark?.global_score;
|
|
const isSelected = device.id === selectedDeviceId;
|
|
|
|
const scoreText = globalScore !== null && globalScore !== undefined
|
|
? Math.round(globalScore)
|
|
: 'N/A';
|
|
|
|
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('');
|
|
}
|
|
|
|
// Select device and display details
|
|
async function selectDevice(deviceId) {
|
|
selectedDeviceId = deviceId;
|
|
renderDeviceList(); // Update selection in list
|
|
|
|
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.');
|
|
}
|
|
}
|
|
|
|
// 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 globalScoreHtml = globalScore !== null && globalScore !== undefined
|
|
? `<span class="${window.BenchUtils.getScoreBadgeClass(globalScore)}" style="font-size: 1.5rem; padding: 0.5rem 1rem;">${getScoreBadgeText(globalScore)}</span>`
|
|
: '<span class="badge">N/A</span>';
|
|
|
|
// 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>
|
|
|
|
<!-- 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>'}
|
|
|
|
<!-- 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>
|
|
|
|
<!-- 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>
|
|
`;
|
|
}
|
|
|
|
// 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>
|
|
`;
|
|
}
|
|
|
|
// Initialize devices page
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadDevices();
|
|
|
|
// Refresh every 30 seconds
|
|
setInterval(loadDevices, 30000);
|
|
});
|
|
|
|
// Make selectDevice available globally
|
|
window.selectDevice = selectDevice;
|