// Linux BenchTools - Devices Two-Panel Layout
(function() {
'use strict';
// Use utilities from global scope directly - avoid redeclaration
const utils = window.BenchUtils;
const apiClient = window.BenchAPI;
let allDevices = [];
let selectedDeviceId = null;
let isEditing = false;
let currentDevice = null;
// Load devices
async function loadDevices() {
const listContainer = document.getElementById('deviceList');
try {
console.log('🔄 Loading devices from API...');
console.log('API URL:', apiClient.baseURL);
const data = await apiClient.getDevices({ page_size: 100 }); // Get all devices (max allowed)
console.log('✅ Devices loaded:', data);
allDevices = data.items || [];
if (allDevices.length === 0) {
listContainer.innerHTML = '
📊
Aucun device
';
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;
});
console.log('📋 Rendering', allDevices.length, 'devices');
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);
console.error('Error details:', error.message);
listContainer.innerHTML = `
❌ Erreur
${error.message || 'Erreur de chargement'}
Backend: ${apiClient.baseURL}
`;
}
}
// 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
? utils.getScoreBadgeClass(globalScore)
: 'badge';
return `
${utils.escapeHtml(device.hostname)}
${scoreText}
${device.last_benchmark?.run_at ? `
⏱️ ${utils.formatRelativeTime(device.last_benchmark.run_at)}
` : '
⚠️ Pas de benchmark
'}
`;
}).join('');
}
// Select device and display details
async function selectDevice(deviceId) {
selectedDeviceId = deviceId;
renderDeviceList(); // Update selection in list
const detailsContainer = document.getElementById('deviceDetailsContainer');
detailsContainer.innerHTML = 'Chargement des détails...
';
try {
const device = await apiClient.getDevice(deviceId);
renderDeviceDetails(device);
} catch (error) {
console.error('Failed to load device details:', error);
utils.showError(detailsContainer, 'Impossible de charger les détails du device.');
}
}
// Toggle edit mode
function toggleEditMode() {
isEditing = !isEditing;
if (currentDevice) {
renderDeviceDetails(currentDevice);
}
}
// Save device changes
async function saveDevice() {
if (!currentDevice) return;
const description = document.getElementById('edit-description')?.value || '';
const location = document.getElementById('edit-location')?.value || '';
const owner = document.getElementById('edit-owner')?.value || '';
const assetTag = document.getElementById('edit-asset-tag')?.value || '';
const tags = document.getElementById('edit-tags')?.value || '';
try {
console.log('💾 Saving device changes...');
const updateData = {
description: description || null,
location: location || null,
owner: owner || null,
asset_tag: assetTag || null,
tags: tags || null
};
await apiClient.updateDevice(currentDevice.id, updateData);
console.log('✅ Device updated successfully');
// Reload device data
isEditing = false;
const updatedDevice = await apiClient.getDevice(currentDevice.id);
currentDevice = updatedDevice;
renderDeviceDetails(updatedDevice);
// Reload device list to reflect changes
await loadDevices();
} catch (error) {
console.error('❌ Failed to save device:', error);
alert('Erreur lors de la sauvegarde: ' + (error.message || 'Erreur inconnue'));
}
}
// Upload image for device
async function uploadImage() {
if (!currentDevice) return;
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
// Check file size (max 10MB)
if (file.size > 10 * 1024 * 1024) {
alert('L\'image est trop volumineuse (max 10MB)');
return;
}
try {
console.log('📤 Uploading image:', file.name);
await apiClient.uploadDocument(currentDevice.id, file, 'image');
console.log('✅ Image uploaded successfully');
// Reload device data to show the new image
const updatedDevice = await apiClient.getDevice(currentDevice.id);
currentDevice = updatedDevice;
renderDeviceDetails(updatedDevice);
} catch (error) {
console.error('❌ Failed to upload image:', error);
alert('Erreur lors du chargement de l\'image: ' + (error.message || 'Erreur inconnue'));
}
};
input.click();
}
// Upload PDF for device
async function uploadPDF() {
if (!currentDevice) return;
const input = document.createElement('input');
input.type = 'file';
input.accept = 'application/pdf';
input.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
// Check file size (max 50MB)
if (file.size > 50 * 1024 * 1024) {
alert('Le PDF est trop volumineux (max 50MB)');
return;
}
try {
console.log('📤 Uploading PDF:', file.name);
await apiClient.uploadDocument(currentDevice.id, file, 'manual');
console.log('✅ PDF uploaded successfully');
// Reload device data to show the new PDF
const updatedDevice = await apiClient.getDevice(currentDevice.id);
currentDevice = updatedDevice;
renderDeviceDetails(updatedDevice);
} catch (error) {
console.error('❌ Failed to upload PDF:', error);
alert('Erreur lors du chargement du PDF: ' + (error.message || 'Erreur inconnue'));
}
};
input.click();
}
// Delete document
async function deleteDocument(docId) {
if (!currentDevice) return;
if (!confirm('Voulez-vous vraiment supprimer ce document ?')) {
return;
}
try {
console.log('🗑️ Deleting document:', docId);
await apiClient.deleteDocument(docId);
console.log('✅ Document deleted successfully');
// Reload device data
const updatedDevice = await apiClient.getDevice(currentDevice.id);
currentDevice = updatedDevice;
renderDeviceDetails(updatedDevice);
} catch (error) {
console.error('❌ Failed to delete document:', error);
alert('Erreur lors de la suppression: ' + (error.message || 'Erreur inconnue'));
}
}
// Helper: Render image documents
function renderImageDocuments(documents) {
if (!documents || !Array.isArray(documents)) {
return '🖼️ Aucune image';
}
const imageDoc = documents.find(doc => doc.doc_type === 'image');
if (!imageDoc) {
return '🖼️ Aucune image';
}
const downloadUrl = apiClient.getDocumentDownloadUrl(imageDoc.id);
return `
📎 ${utils.escapeHtml(imageDoc.filename)}
`;
}
// Helper: Render PDF documents
function renderPDFDocuments(documents) {
if (!documents || !Array.isArray(documents)) {
return '📄 Aucun PDF';
}
const pdfDocs = documents.filter(doc => doc.doc_type === 'manual');
if (pdfDocs.length === 0) {
return '📄 Aucun PDF';
}
return pdfDocs.map(doc => {
const downloadUrl = apiClient.getDocumentDownloadUrl(doc.id);
const uploadDate = new Date(doc.uploaded_at).toLocaleDateString('fr-FR');
return `
📄 ${utils.escapeHtml(doc.filename)}
Uploadé le ${uploadDate}
`;
}).join('');
}
// Render device details (right panel)
function renderDeviceDetails(device) {
currentDevice = 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é)
Utilisée: ${Math.round(ramUsedMB / 1024)}GB •
Libre: ${Math.round(ramFreeMB / 1024)}GB${ramSharedMB > 0 ? ` • Partagée: ${Math.round(ramSharedMB / 1024)}GB` : ''}
`;
}
// 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
? `${utils.getScoreBadgeText(globalScore)}`
: 'N/A';
// 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
? 'WoL ✓'
: 'WoL ✗';
return `
${typeIcon} ${utils.escapeHtml(iface.name)} (${iface.type})${wolBadge}
IP: ${iface.ip || 'N/A'} • MAC: ${iface.mac || 'N/A'}
Vitesse: ${iface.speed_mbps ? iface.speed_mbps + ' Mbps' : 'N/A'}
${iface.driver ? ` • Driver: ${iface.driver}` : ''}
`;
}).join('');
} catch (e) {
networkHtml = 'Erreur de parsing JSON
';
}
}
// Network benchmark results (iperf3)
let netBenchHtml = '';
if (bench?.network_results_json) {
try {
const netResults = JSON.parse(bench.network_results_json);
netBenchHtml = `
📈 Résultats Benchmark Réseau (iperf3)
↑ ${netResults.upload_mbps?.toFixed(2) || 'N/A'}
Upload Mbps
↓ ${netResults.download_mbps?.toFixed(2) || 'N/A'}
Download Mbps
${netResults.ping_ms?.toFixed(2) || 'N/A'}
Ping ms
${netResults.score?.toFixed(2) || 'N/A'}
Score
`;
} catch (e) {
console.error('Error parsing network results:', e);
}
}
detailsContainer.innerHTML = `
${utils.escapeHtml(device.hostname)}
${isEditing ? `
` : `
${utils.escapeHtml(device.description || 'Aucune description')}
`}
${globalScoreHtml}
${!isEditing ? `
` : `
`}
Caractéristiques
${createFormRow('CPU', utils.escapeHtml(cpuModel))}
${createFormRow('Cores / Threads', `${cpuCores} / ${cpuThreads}`)}
${createFormRow('RAM Total', `${ramTotalGB} GB`)}
${createFormRow('RAM Utilisée', ramUsedMB > 0 ? `${Math.round(ramUsedMB / 1024)} GB (${Math.round((ramUsedMB / (snapshot.ram_total_mb || 1)) * 100)}%)` : 'N/A')}
${createFormRow('RAM Libre', ramFreeMB > 0 ? `${Math.round(ramFreeMB / 1024)} GB` : 'N/A')}
${ramSharedMB > 0 ? createFormRow('RAM Partagée', `${Math.round(ramSharedMB / 1024)} GB`) : ''}
${createFormRow('GPU', utils.escapeHtml(gpuSummary))}
${createFormRow('Storage', utils.escapeHtml(storage))}
${createFormRow('OS', utils.escapeHtml(osName))}
${createFormRow('Kernel', utils.escapeHtml(kernelVersion))}
${isEditing ? `
` : `
${device.location ? createFormRow('Location', utils.escapeHtml(device.location)) : ''}
${device.owner ? createFormRow('Propriétaire', utils.escapeHtml(device.owner)) : ''}
${device.asset_tag ? createFormRow('Asset Tag', utils.escapeHtml(device.asset_tag)) : ''}
${device.tags ? createFormRow('Tags', utils.escapeHtml(device.tags)) : ''}
`}
${createFormRow('Créé le', new Date(device.created_at).toLocaleDateString('fr-FR', { year: 'numeric', month: 'long', day: 'numeric' }))}
Image
${renderImageDocuments(device.documents)}
Notice PDF
${renderPDFDocuments(device.documents)}
${bench ? `
📊 Benchmark Scores
${createFormRow('CPU Score', cpuScore !== null && cpuScore !== undefined ? Math.round(cpuScore) : 'N/A', true)}
${createFormRow('RAM Score', memScore !== null && memScore !== undefined ? Math.round(memScore) : 'N/A', true)}
${createFormRow('Disk Score', diskScore !== null && diskScore !== undefined ? Math.round(diskScore) : 'N/A', true)}
${createFormRow('Network Score', netScore !== null && netScore !== undefined ? Math.round(netScore) : 'N/A', true)}
${createFormRow('GPU Score', gpuScore !== null && gpuScore !== undefined ? Math.round(gpuScore) : 'N/A', true)}
⏱️ Dernier benchmark: ${bench.run_at ? utils.formatRelativeTime(bench.run_at) : 'N/A'}
` : ''}
${networkHtml || netBenchHtml ? `
🌐 Détails Réseau
${networkHtml}
${netBenchHtml}
` : ''}
`;
// Attach event listeners for edit/save/upload buttons
setTimeout(() => {
const btnEdit = document.getElementById('btn-edit');
const btnSave = document.getElementById('btn-save');
const btnCancel = document.getElementById('btn-cancel');
const btnUploadImage = document.getElementById('btn-upload-image');
const btnUploadPDF = document.getElementById('btn-upload-pdf');
if (btnEdit) {
btnEdit.addEventListener('click', toggleEditMode);
}
if (btnSave) {
btnSave.addEventListener('click', saveDevice);
}
if (btnCancel) {
btnCancel.addEventListener('click', () => {
isEditing = false;
renderDeviceDetails(currentDevice);
});
}
if (btnUploadImage) {
btnUploadImage.addEventListener('click', uploadImage);
}
if (btnUploadPDF) {
btnUploadPDF.addEventListener('click', uploadPDF);
}
}, 0);
}
// 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
? utils.getScoreBadgeClass(score)
: 'badge';
return `
${icon}
${label}
${scoreValue}
`;
}
// Create info card
function createInfoCard(label, value) {
return `
`;
}
// Create form row (label: value)
function createFormRow(label, value, inline = false) {
if (inline) {
return `
`;
}
return `
`;
}
// Initialize devices page
document.addEventListener('DOMContentLoaded', () => {
loadDevices();
// Refresh every 30 seconds
setInterval(loadDevices, 30000);
});
// Make functions available globally for onclick handlers
window.selectDevice = selectDevice;
window.deleteDocument = deleteDocument;
})(); // End of IIFE