// 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 `
Device image
📎 ${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}
⬇️ Télécharger
`; }).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 ? `
Location
Propriétaire
Asset Tag
Tags
` : ` ${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}
` : ''}
📄 Voir la page complète avec tous les détails
`; // 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 `
${label}
${value}
`; } // Create form row (label: value) function createFormRow(label, value, inline = false) { if (inline) { return `
${label}
${value}
`; } return `
${label}
${value}
`; } // 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