// 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; let editingNotes = false; let editingUpgradeNotes = false; let editingPurchase = false; // Section icon mapping - uses data-icon with IconManager const SECTION_ICON_NAMES = { motherboard: 'motherboard', cpu: 'cpu', ram: 'memory', storage: 'hdd', gpu: 'gpu', network: 'network', usb: 'usb', pci: 'pci', os: 'desktop', shares: 'folder', benchmarks: 'chart-line', metadata: 'info-circle', images: 'image', pdf: 'file-pdf', links: 'link', tags: 'tag', notes: 'edit', purchase: 'shopping-cart', upgrade: 'rocket' }; function getSectionIcon(key, altText) { const iconName = SECTION_ICON_NAMES[key]; if (!iconName) return ''; const safeAlt = utils.escapeHtml(altText || key); return ``; } // 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 hostnameEscaped = (device.hostname || '').replace(/'/g, "\\'").replace(/"/g, '\\"'); const scoreText = formatScoreValue(globalScore); const scoreClass = globalScore !== null && globalScore !== undefined ? utils.getScoreBadgeClass(globalScore) : 'badge'; return `
${utils.escapeHtml(device.hostname || 'N/A')}
${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')); } } async function deleteCurrentDevice() { if (!currentDevice) return; const confirmed = confirm(`Voulez-vous vraiment supprimer le device "${currentDevice.hostname}" ? Cette action supprimera également ses benchmarks et documents.`); if (!confirmed) { return; } try { await apiClient.deleteDevice(currentDevice.id); utils.showToast('Device supprimé', 'success'); currentDevice = null; selectedDeviceId = null; document.getElementById('deviceDetailsContainer').innerHTML = renderEmptyDetailsPlaceholder(); await loadDevices(); } catch (error) { console.error('❌ Failed to delete device:', error); alert('Suppression impossible: ' + (error.message || 'Erreur inconnue')); } } async function deleteDeviceFromList(event, deviceId, hostname) { event.stopPropagation(); const label = hostname || 'ce device'; const confirmed = confirm(`Supprimer définitivement "${label}" ? Toutes les données associées seront perdues.`); if (!confirmed) { return; } try { await apiClient.deleteDevice(deviceId); utils.showToast('Device supprimé', 'success'); if (currentDevice && currentDevice.id === deviceId) { currentDevice = null; selectedDeviceId = null; document.getElementById('deviceDetailsContainer').innerHTML = renderEmptyDetailsPlaceholder(); } await loadDevices(); } catch (error) { console.error('❌ Failed to delete device from list:', error); alert('Suppression impossible: ' + (error.message || 'Erreur inconnue')); } } async function reloadCurrentDevice() { if (!currentDevice) return; const refreshed = await apiClient.getDevice(currentDevice.id); currentDevice = refreshed; renderDeviceDetails(refreshed); } function startNotesEdit() { if (!currentDevice) return; editingNotes = true; renderDeviceDetails(currentDevice); } function cancelNotesEdit() { editingNotes = false; renderDeviceDetails(currentDevice); } async function saveNotes() { if (!currentDevice) return; const textarea = document.getElementById('notes-editor'); const value = textarea ? textarea.value.trim() : ''; try { await apiClient.updateDevice(currentDevice.id, { description: value || null }); editingNotes = false; await reloadCurrentDevice(); utils.showToast('Notes sauvegardées', 'success'); } catch (error) { console.error('Failed to save notes:', error); utils.showToast(error.message || 'Échec de la sauvegarde des notes', 'error'); } } async function clearNotes() { if (!currentDevice) return; if (!confirm('Supprimer les notes ?')) return; try { await apiClient.updateDevice(currentDevice.id, { description: null }); editingNotes = false; await reloadCurrentDevice(); utils.showToast('Notes supprimées', 'success'); } catch (error) { console.error('Failed to clear notes:', error); utils.showToast(error.message || 'Suppression impossible', 'error'); } } function startUpgradeNotesEdit() { if (!currentDevice) return; editingUpgradeNotes = true; renderDeviceDetails(currentDevice); } function cancelUpgradeNotesEdit() { editingUpgradeNotes = false; renderDeviceDetails(currentDevice); } async function saveUpgradeNotes() { if (!currentDevice || !currentDevice.last_benchmark) return; const textarea = document.getElementById('upgrade-notes-editor'); const value = textarea ? textarea.value.trim() : ''; try { await apiClient.updateBenchmark(currentDevice.last_benchmark.id, { notes: value || null }); editingUpgradeNotes = false; await reloadCurrentDevice(); utils.showToast('Upgrade notes sauvegardées', 'success'); } catch (error) { console.error('Failed to save upgrade notes:', error); utils.showToast(error.message || 'Échec de la sauvegarde des upgrade notes', 'error'); } } async function clearUpgradeNotes() { if (!currentDevice || !currentDevice.last_benchmark) return; if (!confirm('Supprimer les upgrade notes ?')) return; try { await apiClient.updateBenchmark(currentDevice.last_benchmark.id, { notes: null }); editingUpgradeNotes = false; await reloadCurrentDevice(); utils.showToast('Upgrade notes supprimées', 'success'); } catch (error) { console.error('Failed to clear upgrade notes:', error); utils.showToast(error.message || 'Suppression impossible', 'error'); } } function startPurchaseEdit() { if (!currentDevice) return; editingPurchase = true; renderDeviceDetails(currentDevice); } function cancelPurchaseEdit() { editingPurchase = false; renderDeviceDetails(currentDevice); } async function savePurchaseInfo() { if (!currentDevice) return; const storeInput = document.getElementById('purchase-store-input'); const dateInput = document.getElementById('purchase-date-input'); const priceInput = document.getElementById('purchase-price-input'); const store = storeInput ? storeInput.value.trim() : ''; const date = dateInput ? dateInput.value.trim() : ''; const priceValue = priceInput ? priceInput.value.trim() : ''; const price = priceValue !== '' && !Number.isNaN(Number(priceValue)) ? Number(priceValue) : null; try { await apiClient.updateDevice(currentDevice.id, { purchase_store: store || null, purchase_date: date || null, purchase_price: price }); editingPurchase = false; await reloadCurrentDevice(); utils.showToast('Informations d’achat mises à jour', 'success'); } catch (error) { console.error('Failed to save purchase info:', error); utils.showToast(error.message || 'Échec de la sauvegarde', 'error'); } } async function addTag() { if (!currentDevice) return; const input = document.getElementById(`new-tag-input-${currentDevice.id}`); if (!input) return; const value = input.value.trim(); if (!value) return; const tags = utils.parseTags(currentDevice.tags); if (tags.includes(value)) { utils.showToast('Tag déjà présent', 'warning'); return; } tags.push(value); try { await apiClient.updateDevice(currentDevice.id, { tags: tags.join(',') }); input.value = ''; await reloadCurrentDevice(); } catch (error) { console.error('Failed to add tag:', error); utils.showToast(error.message || 'Ajout impossible', 'error'); } } async function removeTag(tagValue) { if (!currentDevice) return; const tags = utils.parseTags(currentDevice.tags).filter(tag => tag !== tagValue); try { await apiClient.updateDevice(currentDevice.id, { tags: tags.length ? tags.join(',') : null }); await reloadCurrentDevice(); } catch (error) { console.error('Failed to remove tag:', error); utils.showToast(error.message || 'Suppression du tag impossible', 'error'); } } async function addDeviceLinkEntry(deviceId) { const labelInput = document.getElementById(`link-label-${deviceId}`); const urlInput = document.getElementById(`link-url-${deviceId}`); if (!labelInput || !urlInput) return; const label = labelInput.value.trim(); const url = urlInput.value.trim(); if (!label || !url) { utils.showToast('Libellé et URL requis', 'warning'); return; } try { await apiClient.addDeviceLink(deviceId, { label, url }); labelInput.value = ''; urlInput.value = ''; utils.showToast('Lien ajouté', 'success'); loadLinksSection(deviceId); } catch (error) { console.error('Failed to add link:', error); utils.showToast(error.message || 'Ajout du lien impossible', 'error'); } } async function deleteDeviceLink(linkId, deviceId) { if (!confirm('Supprimer ce lien ?')) return; try { await apiClient.deleteLink(linkId); utils.showToast('Lien supprimé', 'success'); loadLinksSection(deviceId); } catch (error) { console.error('Failed to delete link:', error); utils.showToast(error.message || 'Suppression impossible', 'error'); } } // 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 imageDocs = documents.filter(doc => doc.doc_type === 'image'); if (imageDocs.length === 0) { return '🖼️ Aucune image'; } return `
${imageDocs.map(imageDoc => { const downloadUrl = apiClient.getDocumentDownloadUrl(imageDoc.id); const safeFilenameArg = utils.escapeHtml(JSON.stringify(imageDoc.filename || '')); return `
${utils.escapeHtml(imageDoc.filename)}
📎 ${utils.escapeHtml(imageDoc.filename.substring(0, 15))}${imageDoc.filename.length > 15 ? '...' : ''}
`; }).join('')}
`; } // 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(''); } // Helper: Render Motherboard Details function renderMotherboardDetails(snapshot) { if (!snapshot) { return '

Aucune information disponible

'; } const cleanValue = (val) => { if (!val || (typeof val === 'string' && val.trim() === '')) return 'N/A'; return val; }; const items = [ { label: 'Fabricant', value: cleanValue(snapshot.motherboard_vendor) }, { label: 'Modèle', value: cleanValue(snapshot.motherboard_model) }, { label: 'BIOS Vendor', value: cleanValue(snapshot.bios_vendor) }, { label: 'Version BIOS', value: cleanValue(snapshot.bios_version) }, { label: 'Date BIOS', value: cleanValue(snapshot.bios_date) }, { label: 'Slots RAM', value: `${snapshot.ram_slots_used || '?'} utilisés / ${snapshot.ram_slots_total || '?'} total` } ]; return `
${items.map(item => `
${item.label}
${utils.escapeHtml(String(item.value))}
`).join('')}
`; } // Helper: Render CPU Details function renderCPUDetails(snapshot) { if (!snapshot) { return '

Aucune information disponible

'; } const items = [ { label: 'Fabricant', value: snapshot.cpu_vendor || 'N/A', tooltip: snapshot.cpu_model }, { label: 'Modèle', value: snapshot.cpu_model || 'N/A', tooltip: snapshot.cpu_microarchitecture ? `Architecture: ${snapshot.cpu_microarchitecture}` : null }, { label: 'Microarchitecture', value: snapshot.cpu_microarchitecture || 'N/A' }, { label: 'Cores', value: snapshot.cpu_cores != null ? snapshot.cpu_cores : 'N/A', tooltip: snapshot.cpu_threads ? `${snapshot.cpu_threads} threads disponibles` : null }, { label: 'Threads', value: snapshot.cpu_threads != null ? snapshot.cpu_threads : 'N/A', tooltip: snapshot.cpu_cores ? `${snapshot.cpu_cores} cores physiques` : null }, { label: 'Fréquence de base', value: snapshot.cpu_base_freq_ghz ? `${snapshot.cpu_base_freq_ghz} GHz` : 'N/A', tooltip: snapshot.cpu_max_freq_ghz ? `Max: ${snapshot.cpu_max_freq_ghz} GHz` : null }, { label: 'Fréquence max', value: snapshot.cpu_max_freq_ghz ? `${snapshot.cpu_max_freq_ghz} GHz` : 'N/A', tooltip: snapshot.cpu_base_freq_ghz ? `Base: ${snapshot.cpu_base_freq_ghz} GHz` : null }, { label: 'TDP', value: snapshot.cpu_tdp_w ? `${snapshot.cpu_tdp_w} W` : 'N/A', tooltip: 'Thermal Design Power - Consommation thermique typique' }, { label: 'Cache L1', value: snapshot.cpu_cache_l1_kb ? utils.formatCache(snapshot.cpu_cache_l1_kb) : 'N/A', tooltip: 'Cache de niveau 1 - Le plus rapide' }, { label: 'Cache L2', value: snapshot.cpu_cache_l2_kb ? utils.formatCache(snapshot.cpu_cache_l2_kb) : 'N/A', tooltip: 'Cache de niveau 2 - Intermédiaire' }, { label: 'Cache L3', value: snapshot.cpu_cache_l3_kb ? utils.formatCache(snapshot.cpu_cache_l3_kb) : 'N/A', tooltip: 'Cache de niveau 3 - Partagé entre les cores' } ]; let html = `
${items.map(item => `
${item.label}
${utils.escapeHtml(String(item.value))}
`).join('')}
`; // CPU Flags if (snapshot.cpu_flags) { try { const flags = typeof snapshot.cpu_flags === 'string' ? JSON.parse(snapshot.cpu_flags) : snapshot.cpu_flags; if (Array.isArray(flags) && flags.length > 0) { html += `
Instructions supportées:
${flags.slice(0, 30).map(flag => `${utils.escapeHtml(flag)}`).join('')} ${flags.length > 30 ? `+${flags.length - 30} autres...` : ''}
`; } } catch (e) { console.warn('Failed to parse CPU flags:', e); } } return html; } // Helper: Render Memory Details function renderMemoryDetails(snapshot) { if (!snapshot) { return '

Aucune information disponible

'; } const items = [ { label: 'RAM totale', value: snapshot.ram_total_mb ? utils.formatMemory(snapshot.ram_total_mb) : 'N/A' }, { label: 'RAM utilisée', value: snapshot.ram_used_mb ? utils.formatMemory(snapshot.ram_used_mb) : 'N/A' }, { label: 'RAM libre', value: snapshot.ram_free_mb ? utils.formatMemory(snapshot.ram_free_mb) : 'N/A' }, { label: 'RAM partagée', value: snapshot.ram_shared_mb ? utils.formatMemory(snapshot.ram_shared_mb) : 'N/A' }, { label: 'Slots utilisés', value: snapshot.ram_slots_used != null ? snapshot.ram_slots_used : 'N/A' }, { label: 'Slots total', value: snapshot.ram_slots_total != null ? snapshot.ram_slots_total : 'N/A' }, { label: 'ECC', value: snapshot.ram_ecc != null ? (snapshot.ram_ecc ? 'Oui' : 'Non') : 'N/A' } ]; let html = `
${items.map(item => `
${item.label}
${utils.escapeHtml(String(item.value))}
`).join('')}
`; // RAM Layout (if available) if (snapshot.ram_layout_json) { try { const layout = typeof snapshot.ram_layout_json === 'string' ? JSON.parse(snapshot.ram_layout_json) : snapshot.ram_layout_json; if (Array.isArray(layout) && layout.length > 0) { html += `
Configuration des barrettes:
${layout.map(slot => `
${utils.escapeHtml(slot.slot || 'N/A')}: ${utils.formatMemory(slot.size_mb || 0)} ${slot.type ? `(${utils.escapeHtml(slot.type)})` : ''} ${slot.speed_mhz ? `@ ${slot.speed_mhz} MHz` : ''}
`).join('')}
`; } } catch (e) { console.warn('Failed to parse RAM layout:', e); } } return html; } // Helper: Render Storage Details function renderStorageDetails(snapshot) { if (!snapshot) { return '

Aucune information disponible

'; } let html = ''; try { if (snapshot.storage_devices_json) { const devices = typeof snapshot.storage_devices_json === 'string' ? JSON.parse(snapshot.storage_devices_json) : snapshot.storage_devices_json; if (Array.isArray(devices) && devices.length > 0) { html += `
${devices.map(disk => { const typeIcon = disk.type === 'SSD' ? '💾' : '💿'; const healthColor = disk.smart_health === 'PASSED' ? 'var(--color-success)' : disk.smart_health === 'FAILED' ? 'var(--color-danger)' : 'var(--text-secondary)'; return `
${typeIcon} ${utils.escapeHtml(disk.name || disk.device || 'N/A')}
${utils.escapeHtml(disk.model || 'Unknown model')}
${disk.smart_health ? ` ${utils.escapeHtml(disk.smart_health)} ` : ''}
${disk.capacity_gb ? `
Capacité: ${utils.formatStorage(disk.capacity_gb)}
` : ''} ${disk.type ? `
Type: ${utils.escapeHtml(disk.type)}
` : ''} ${disk.interface ? `
Interface: ${utils.escapeHtml(disk.interface)}
` : ''} ${disk.temperature_c ? `
Température: ${utils.formatTemperature(disk.temperature_c)}
` : ''}
`; }).join('')}
`; } else { html += '

Aucun disque détecté

'; } } } catch (e) { console.error('Failed to parse storage devices:', e); html += '

Erreur lors du parsing des données de stockage

'; } if (snapshot.partitions_json) { try { const partitions = typeof snapshot.partitions_json === 'string' ? JSON.parse(snapshot.partitions_json) : snapshot.partitions_json; if (Array.isArray(partitions) && partitions.length > 0) { html += `

Partitions

${partitions.map(part => { const used = typeof part.used_gb === 'number' ? utils.formatStorage(part.used_gb) : 'N/A'; const free = typeof part.free_gb === 'number' ? utils.formatStorage(part.free_gb) : (typeof part.total_gb === 'number' && typeof part.used_gb === 'number' ? utils.formatStorage(part.total_gb - part.used_gb) : 'N/A'); const total = typeof part.total_gb === 'number' ? utils.formatStorage(part.total_gb) : 'N/A'; return ` `; }).join('')}
Partition Montage Type Utilisé Libre Total Usage
${utils.escapeHtml(part.name || 'N/A')} ${part.mount_point ? utils.escapeHtml(part.mount_point) : 'Non monté'} ${part.fs_type ? utils.escapeHtml(part.fs_type) : 'N/A'} ${used} ${free} ${total} ${renderUsageBadge(part.used_gb, part.total_gb)}
`; } } catch (error) { console.error('Failed to parse partitions:', error); html += '

Erreur lors de la lecture des partitions

'; } } if (!html) { html = '

Aucune information disponible

'; } return html; } // Helper: Render GPU Details function renderGPUDetails(snapshot) { if (!snapshot) { return '

Aucune information disponible

'; } if (!snapshot.gpu_vendor && !snapshot.gpu_model && !snapshot.gpu_summary) { return '

Aucun GPU détecté

'; } const items = [ { label: 'Fabricant', value: snapshot.gpu_vendor || 'N/A' }, { label: 'Modèle', value: snapshot.gpu_model || snapshot.gpu_summary || 'N/A' } ]; return `
${items.map(item => `
${item.label}
${utils.escapeHtml(String(item.value))}
`).join('')}
`; } // Helper: Render OS Details function renderOSDetails(snapshot) { if (!snapshot) { return '

Aucune information disponible

'; } const displayServer = snapshot.display_server || snapshot.session_type || 'N/A'; const resolution = snapshot.screen_resolution || 'N/A'; const lastBoot = snapshot.last_boot_time || 'N/A'; const uptime = snapshot.uptime_seconds != null ? utils.formatDuration(snapshot.uptime_seconds) : 'N/A'; const battery = snapshot.battery_percentage != null ? `${snapshot.battery_percentage}%${snapshot.battery_status ? ` (${snapshot.battery_status})` : ''}` : 'N/A'; const items = [ { label: 'Hostname', value: snapshot.hostname || 'N/A' }, { label: 'Distribution', value: snapshot.os_name || 'N/A' }, { label: 'Version', value: snapshot.os_version || 'N/A' }, { label: 'Kernel', value: snapshot.kernel_version || 'N/A' }, { label: 'Architecture', value: snapshot.architecture || 'N/A' }, { label: 'Environnement', value: snapshot.desktop_environment || 'N/A' }, { label: 'Session / Display', value: displayServer }, { label: 'Résolution écran', value: resolution }, { label: 'Dernier boot', value: lastBoot }, { label: 'Uptime', value: uptime }, { label: 'Batterie', value: battery }, { label: 'Virtualisation', value: snapshot.virtualization_type || 'none' } ]; return `
${items.map(item => `
${item.label}
${utils.escapeHtml(String(item.value))}
`).join('')}
`; } // Helper: Render PCI Devices function renderPCIDetails(snapshot) { if (!snapshot || !snapshot.pci_devices_json) { return '

Aucune information disponible

'; } try { const devices = typeof snapshot.pci_devices_json === 'string' ? JSON.parse(snapshot.pci_devices_json) : snapshot.pci_devices_json; if (!Array.isArray(devices) || devices.length === 0) { return '

Aucun périphérique PCI détecté

'; } return `
${devices.map(dev => `
Slot:
${utils.escapeHtml(dev.slot || 'N/A')}
Class:
${utils.escapeHtml(dev.class || 'N/A')}
Vendor:
${utils.escapeHtml(dev.vendor || 'N/A')}
Device:
${utils.escapeHtml(dev.device || 'N/A')}
`).join('')}
`; } catch (e) { console.error('Failed to parse PCI devices:', e); return '

Erreur lors du parsing des données PCI

'; } } // Helper: Render USB Devices function renderUSBDetails(snapshot) { if (!snapshot || !snapshot.usb_devices_json) { return '

Aucune information disponible

'; } try { const devices = typeof snapshot.usb_devices_json === 'string' ? JSON.parse(snapshot.usb_devices_json) : snapshot.usb_devices_json; if (!Array.isArray(devices) || devices.length === 0) { return '

Aucun périphérique USB détecté

'; } return `
${devices.map(dev => `
${utils.escapeHtml(dev.name || 'Unknown USB Device')}
Bus/Device:
${utils.escapeHtml(dev.bus || 'N/A')} / ${utils.escapeHtml(dev.device || 'N/A')}
Vendor ID:
${utils.escapeHtml(dev.vendor_id || 'N/A')}
Product ID:
${utils.escapeHtml(dev.product_id || 'N/A')}
`).join('')}
`; } catch (e) { console.error('Failed to parse USB devices:', e); return '

Erreur lors du parsing des données USB

'; } } // Helper: View benchmark details async function viewBenchmarkDetails(benchmarkId) { const modalBody = document.getElementById('benchmarkModalBody'); utils.openModal('benchmarkModal'); try { const benchmark = await apiClient.getBenchmark(benchmarkId); modalBody.innerHTML = `
${JSON.stringify(benchmark.details || benchmark, null, 2)}
`; } catch (error) { console.error('Failed to load benchmark details:', error); modalBody.innerHTML = `
Erreur: ${utils.escapeHtml(error.message)}
`; } } // Render IP Display with edit capability function renderIPDisplay(snapshot, device) { // Extract non-loopback IPs const networkInterfaces = snapshot?.network_interfaces_json ? (typeof snapshot.network_interfaces_json === 'string' ? JSON.parse(snapshot.network_interfaces_json) : snapshot.network_interfaces_json) : []; const ips = networkInterfaces .filter(iface => iface.ipv4 && iface.ipv4 !== '127.0.0.1' && iface.ipv4 !== 'N/A') .map(iface => iface.ipv4); const displayIP = ips.length > 0 ? ips.join(', ') : 'N/A'; const ipUrl = device.ip_url || (ips.length > 0 ? `http://${ips[0]}` : ''); return `
${ipUrl ? `${utils.escapeHtml(displayIP)}` : `${utils.escapeHtml(displayIP)}`}
`; } async function editIPUrl() { const editor = document.getElementById('ip-url-editor'); const btnEdit = document.getElementById('btn-edit-ip-url'); if (!editor || !btnEdit) return; editor.style.display = 'block'; btnEdit.style.display = 'none'; document.getElementById('ip-url-input')?.focus(); } async function saveIPUrl() { if (!currentDevice) return; const input = document.getElementById('ip-url-input'); if (!input) return; let url = input.value.trim(); // Auto-prefix http:// if not present and not empty if (url && !url.match(/^https?:\/\//)) { url = `http://${url}`; } try { await apiClient.updateDevice(currentDevice.id, { ip_url: url || null }); utils.showToast('Lien IP sauvegardé', 'success'); await reloadCurrentDevice(); } catch (error) { console.error('Failed to save IP URL:', error); utils.showToast(error.message || 'Échec de la sauvegarde du lien IP', 'error'); } } async function cancelIPUrlEdit() { if (!currentDevice) return; const editor = document.getElementById('ip-url-editor'); const btnEdit = document.getElementById('btn-edit-ip-url'); if (!editor || !btnEdit) return; editor.style.display = 'none'; btnEdit.style.display = 'inline-block'; // Reset input value const input = document.getElementById('ip-url-input'); if (input) { input.value = currentDevice.ip_url || ''; } } // Search model on web function searchModelOnWeb() { const btn = document.getElementById('btn-search-model'); if (!btn) return; const model = btn.dataset.model; if (!model || model === 'N/A') { utils.showToast('Aucun modèle à rechercher', 'warning'); return; } // Get search engine from settings (default: Google) const searchEngine = localStorage.getItem('searchEngine') || 'google'; const searchUrls = { google: `https://www.google.com/search?q=${encodeURIComponent(model)}`, duckduckgo: `https://duckduckgo.com/?q=${encodeURIComponent(model)}`, bing: `https://www.bing.com/search?q=${encodeURIComponent(model)}` }; const url = searchUrls[searchEngine] || searchUrls.google; window.open(url, '_blank', 'noopener,noreferrer'); } // Render device details (right panel) function renderDeviceDetails(device) { const previousDeviceId = currentDevice?.id; currentDevice = device; if (previousDeviceId !== device.id) { editingNotes = false; editingUpgradeNotes = false; editingPurchase = false; } const detailsContainer = document.getElementById('deviceDetailsContainer'); const snapshot = device.last_hardware_snapshot; const bench = device.last_benchmark; const metaParts = []; if (device.location) metaParts.push(`📍 ${utils.escapeHtml(device.location)}`); if (device.owner) metaParts.push(`👤 ${utils.escapeHtml(device.owner)}`); if (device.asset_tag) metaParts.push(`🏷️ ${utils.escapeHtml(device.asset_tag)}`); const brand = snapshot?.motherboard_vendor || snapshot?.os_name || 'N/A'; const model = snapshot?.motherboard_model || snapshot?.os_version || 'N/A'; const imageCount = (device.documents || []).filter(doc => doc.doc_type === 'image').length; const imagesContent = renderImageDocuments(device.documents); const headerHtml = `
Hostname
${utils.escapeHtml(device.hostname)}
${metaParts.length > 0 ? metaParts.join(' • ') : 'Aucune métadonnée'}
Adresse IP
${renderIPDisplay(snapshot, device)}
Marque
${utils.escapeHtml(brand)}
Modèle
${utils.escapeHtml(model)}
Score global
${formatScoreValue(bench?.global_score)}
${createSection( 'section-images-header', getSectionIcon('images', 'Images'), 'Images', imagesContent, { actionsHtml: ` ` } )}
`; const metadataActions = isEditing ? ` ` : ` `; const notesActions = editingNotes ? ` ` : ` ${device.description ? ` ` : ''} `; const upgradeActions = currentDevice?.last_benchmark ? (editingUpgradeNotes ? ` ` : ` ${currentDevice.last_benchmark.notes ? ` ` : ''} `) : ''; const purchaseActions = editingPurchase ? ` ` : ` `; const sections = { motherboard: createSection('section-motherboard', getSectionIcon('motherboard', 'Carte Mère'), 'Carte Mère', renderMotherboardDetails(snapshot)), cpu: createSection('section-cpu', getSectionIcon('cpu', 'Processeur'), 'Processeur (CPU)', renderCPUDetails(snapshot)), ram: createSection('section-ram', getSectionIcon('ram', 'Mémoire'), 'Mémoire (RAM)', renderMemoryDetails(snapshot)), disks: createSection('section-disks', getSectionIcon('storage', 'Stockage'), 'Stockage (Disques)', renderStorageDetails(snapshot)), gpu: createSection('section-gpu', getSectionIcon('gpu', 'Carte Graphique'), 'Carte Graphique (GPU)', renderGPUDetails(snapshot)), network: createSection('section-network', getSectionIcon('network', 'Interfaces réseau'), 'Interfaces Réseau', renderNetworkBlock(snapshot, bench)), usb: createSection('section-usb', getSectionIcon('usb', 'USB'), 'Périphériques USB', renderUSBDetails(snapshot)), pci: createSection('section-pci', getSectionIcon('pci', 'PCI'), 'Périphériques PCI', renderPCIDetails(snapshot)), os: createSection('section-os', getSectionIcon('os', 'Système'), 'Système d’exploitation', renderOSDetails(snapshot)), shares: createSection('section-shares', getSectionIcon('shares', 'Partages réseau'), 'Partages réseau', renderNetworkSharesDetails(snapshot)), benchmarks: createSection('section-benchmarks', getSectionIcon('benchmarks', 'Benchmarks'), 'Benchmarks', renderBenchmarkSection(device.id, bench)), metadata: createSection('section-metadata', getSectionIcon('metadata', 'Métadonnées'), 'Métadonnées', renderMetadataSection(device, bench), { actionsHtml: metadataActions }) }; const pdfContent = renderPDFDocuments(device.documents); sections.pdf = createSection( 'section-pdf', getSectionIcon('pdf', 'Notices PDF'), 'Notices PDF', pdfContent, { actionsHtml: ` ` } ); sections.links = createSection('section-links', getSectionIcon('links', 'Liens'), 'URLs & Liens', renderLinksPlaceholder(device.id)); sections.tags = createSection('section-tags', getSectionIcon('tags', 'Tags'), 'Tags', renderTagsSection(device)); sections.notes = createSection('section-notes', getSectionIcon('notes', 'Notes'), 'Notes (Markdown)', renderNotesSection(device), { actionsHtml: notesActions }); sections.purchase = createSection('section-purchase', getSectionIcon('purchase', 'Informations d’achat'), 'Informations d’achat', renderPurchaseSection(device), { actionsHtml: purchaseActions }); sections.upgrades = createSection('section-upgrades', getSectionIcon('upgrade', 'Upgrade Notes'), 'Upgrade Notes', renderUpgradeSection(bench), { actionsHtml: upgradeActions }); const sectionsOrder = [ 'motherboard', 'cpu', 'ram', 'disks', 'gpu', 'network', 'os', 'shares', 'usb', 'pci', 'benchmarks', 'metadata', 'pdf', 'links', 'tags', 'notes', 'purchase', 'upgrades' ]; const orderedSections = sectionsOrder.map(key => sections[key] || '').join(''); detailsContainer.innerHTML = headerHtml + orderedSections; // Initialize icons using IconManager if (window.IconManager) { window.IconManager.inlineSvgIcons(detailsContainer); } bindDetailActions(); loadLinksSection(device.id); loadBenchmarkHistorySection(device.id); } function bindDetailActions() { 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 btnUploadImageHeader = document.getElementById('btn-upload-image-header'); const btnUploadPDF = document.getElementById('btn-upload-pdf'); const btnDelete = document.getElementById('btn-delete'); // IP URL editing const btnEditIpUrl = document.getElementById('btn-edit-ip-url'); const btnSaveIpUrl = document.getElementById('btn-save-ip-url'); const btnCancelIpUrl = document.getElementById('btn-cancel-ip-url'); // Web search const btnSearchModel = document.getElementById('btn-search-model'); 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 (btnUploadImageHeader) btnUploadImageHeader.addEventListener('click', uploadImage); if (btnUploadPDF) btnUploadPDF.addEventListener('click', uploadPDF); if (btnDelete) btnDelete.addEventListener('click', deleteCurrentDevice); // Bind IP URL actions if (btnEditIpUrl) btnEditIpUrl.addEventListener('click', editIPUrl); if (btnSaveIpUrl) btnSaveIpUrl.addEventListener('click', saveIPUrl); if (btnCancelIpUrl) btnCancelIpUrl.addEventListener('click', cancelIPUrlEdit); // Bind web search if (btnSearchModel) btnSearchModel.addEventListener('click', searchModelOnWeb); } async function loadLinksSection(deviceId) { const listContainer = document.getElementById(`links-list-${deviceId}`); if (!listContainer) return; listContainer.innerHTML = '
Chargement des liens...
'; try { const links = await apiClient.getDeviceLinks(deviceId); listContainer.innerHTML = renderLinksList(links, deviceId); } catch (error) { console.error('Failed to load links:', error); listContainer.innerHTML = `

Erreur: ${utils.escapeHtml(error.message || 'Impossible de charger les liens')}

`; } } async function loadBenchmarkHistorySection(deviceId) { const container = document.getElementById(`benchmark-history-${deviceId}`); if (!container) return; try { const data = await apiClient.getDeviceBenchmarks(deviceId, { limit: 20 }); if (!data.items || data.items.length === 0) { container.innerHTML = '

Aucun benchmark dans l\'historique.

'; return; } container.innerHTML = `
${data.items.map(bench => ` `).join('')}
Date Global CPU CPU Mono CPU Multi Mémoire Disque Réseau GPU Version
${utils.escapeHtml(utils.formatDate(bench.run_at))} ${formatScoreValue(bench.global_score)} ${formatScoreValue(bench.cpu_score)} ${formatScoreValue(bench.cpu_score_single)} ${formatScoreValue(bench.cpu_score_multi)} ${formatScoreValue(bench.memory_score)} ${formatScoreValue(bench.disk_score)} ${formatScoreValue(bench.network_score)} ${formatScoreValue(bench.gpu_score)} ${utils.escapeHtml(bench.bench_script_version || 'N/A')}
`; } catch (error) { console.error('Failed to load benchmark history:', error); container.innerHTML = `

Erreur: ${utils.escapeHtml(error.message || 'Impossible de charger l\'historique')}

`; } } // Create score card for display function createScoreCard(score, label, icon) { const scoreValue = formatScoreValue(score); 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}
`; } function createSection(id, icon, title, content, options = {}) { const iconHtml = icon ? `${icon}` : ''; const actionsHtml = options.actionsHtml ? `
${options.actionsHtml}
` : ''; return `

${iconHtml}${title}

${actionsHtml}
${content}
`; } function formatRawValue(value) { if (value === null || value === undefined || value === '') return 'N/A'; if (typeof value === 'number') return value; return utils.escapeHtml(String(value)); } function formatScoreValue(value) { if (value === null || value === undefined || value === '') return 'N/A'; const numeric = Number(value); if (!Number.isFinite(numeric)) { return utils.escapeHtml(String(value)); } return Math.ceil(numeric); } function renderUsageBadge(used, total) { if (typeof used !== 'number' || typeof total !== 'number' || total <= 0) { return 'N/A'; } const percent = Math.min(100, Math.max(0, Math.round((used / total) * 100))); const modifier = percent >= 85 ? 'high' : percent >= 60 ? 'medium' : 'ok'; return `${percent}%`; } function formatPriceValue(value) { if (value === null || value === undefined || value === '') return 'N/A'; const numeric = Number(value); if (Number.isNaN(numeric)) return utils.escapeHtml(String(value)); return `${numeric.toFixed(2)} €`; } function renderMetadataInput(label, id, value, placeholder = '') { return `
${label}
`; } function renderMetadataSection(device, bench) { const rows = []; if (isEditing) { return ` ${renderMetadataInput('Location', 'edit-location', device.location, 'Ex: Datacenter Paris')} ${renderMetadataInput('Propriétaire', 'edit-owner', device.owner, 'Responsable')} ${renderMetadataInput('Asset Tag', 'edit-asset-tag', device.asset_tag, 'INV-00123')} ${createFormRow('Créé le', utils.escapeHtml(new Date(device.created_at).toLocaleString('fr-FR')))} ${createFormRow('Mis à jour', utils.escapeHtml(new Date(device.updated_at).toLocaleString('fr-FR')))} ${createFormRow('Version bench', utils.escapeHtml(bench?.bench_script_version || 'N/A'))} `; } if (device.location) rows.push(createFormRow('Location', utils.escapeHtml(device.location))); if (device.owner) rows.push(createFormRow('Propriétaire', utils.escapeHtml(device.owner))); if (device.asset_tag) rows.push(createFormRow('Asset Tag', utils.escapeHtml(device.asset_tag))); rows.push(createFormRow('Créé le', utils.escapeHtml(new Date(device.created_at).toLocaleString('fr-FR')))); rows.push(createFormRow('Mis à jour', utils.escapeHtml(new Date(device.updated_at).toLocaleString('fr-FR')))); rows.push(createFormRow('Version bench', utils.escapeHtml(bench?.bench_script_version || 'N/A'))); return rows.join(''); } function renderNetworkBlock(snapshot, bench) { if (!snapshot || !snapshot.network_interfaces_json) { return '

Aucune information réseau disponible

'; } let html = ''; try { const interfaces = typeof snapshot.network_interfaces_json === 'string' ? JSON.parse(snapshot.network_interfaces_json) : snapshot.network_interfaces_json; if (!interfaces || interfaces.length === 0) { return '

Aucune interface réseau détectée

'; } const hostLabel = snapshot.hostname || currentDevice?.hostname || null; html += '
'; interfaces.forEach(iface => { const typeIcon = iface.type === 'ethernet' ? '🔌' : (iface.type === 'wifi' ? '📡' : '🌐'); const wol = iface.wake_on_lan; const wolBadge = wol === true ? 'WoL ✓' : (wol === false ? 'WoL ✗' : ''); const networkName = iface.network_name || iface.connection_name || iface.profile || iface.ssid || null; html += `
${typeIcon} ${utils.escapeHtml(iface.name || 'N/A')}
${utils.escapeHtml(iface.type || 'unknown')}
${wolBadge}
${hostLabel ? `
Hostname:
${utils.escapeHtml(hostLabel)}
` : ''} ${networkName ? `
Nom du réseau:
${utils.escapeHtml(networkName)}
` : ''} ${iface.ip ? `
Adresse IP:
${utils.escapeHtml(iface.ip)}
` : ''} ${iface.mac ? `
MAC:
${utils.escapeHtml(iface.mac)}
` : ''} ${iface.speed_mbps ? `
Vitesse:
${iface.speed_mbps} Mbps
` : ''} ${iface.driver ? `
Driver:
${utils.escapeHtml(iface.driver)}
` : ''} ${iface.ssid ? `
SSID:
${utils.escapeHtml(iface.ssid)}
` : ''}
`; }); html += '
'; } catch (error) { console.error('Failed to parse network interfaces:', error); html = '

Erreur lors du parsing des données réseau

'; } if (bench?.network_results_json) { try { const netResults = typeof bench.network_results_json === 'string' ? JSON.parse(bench.network_results_json) : bench.network_results_json; html += `
📈 Résultats iperf3
${formatRawValue(netResults.upload_mbps)}
Upload Mbps
${formatRawValue(netResults.download_mbps)}
Download Mbps
${formatRawValue(netResults.ping_ms)}
Ping ms
${formatScoreValue(netResults.score)}
Score
`; } catch (error) { console.error('Failed to parse network benchmark results:', error); } } return html; } function renderNetworkSharesDetails(snapshot) { if (!snapshot || !snapshot.network_shares_json) { return '

Aucun partage réseau détecté

'; } try { const shares = typeof snapshot.network_shares_json === 'string' ? JSON.parse(snapshot.network_shares_json) : snapshot.network_shares_json; if (!Array.isArray(shares) || shares.length === 0) { return '

Aucun partage réseau monté

'; } return `
${shares.map(share => ` `).join('')}
Source Montage Protocole Type Utilisé Libre Total Options
${utils.escapeHtml(share.source || 'N/A')} ${utils.escapeHtml(share.mount_point || 'N/A')} ${utils.escapeHtml(share.protocol || share.fs_type || 'N/A')} ${share.fs_type ? utils.escapeHtml(share.fs_type) : 'N/A'} ${typeof share.used_gb === 'number' ? utils.formatStorage(share.used_gb, 'GB') : 'N/A'} ${typeof share.free_gb === 'number' ? utils.formatStorage(share.free_gb, 'GB') : 'N/A'} ${typeof share.total_gb === 'number' ? utils.formatStorage(share.total_gb, 'GB') : 'N/A'} ${share.options ? utils.escapeHtml(share.options) : 'N/A'}
`; } catch (error) { console.error('Failed to parse network shares:', error); return '

Erreur lors de la lecture des partages réseau

'; } } function renderBenchmarkSection(deviceId, bench) { if (!bench) { return '

Aucun benchmark disponible pour ce device.

'; } const rows = [ { label: 'Score global', value: formatScoreValue(bench.global_score) }, { label: 'CPU score', value: formatScoreValue(bench.cpu_score) }, { label: 'CPU single', value: formatScoreValue(bench.cpu_score_single) }, { label: 'CPU multi', value: formatScoreValue(bench.cpu_score_multi) }, { label: 'Mémoire', value: formatScoreValue(bench.memory_score) }, { label: 'Disque', value: formatScoreValue(bench.disk_score) }, { label: 'Réseau', value: formatScoreValue(bench.network_score) }, { label: 'GPU', value: formatScoreValue(bench.gpu_score) }, { label: 'Exécuté le', value: utils.escapeHtml(utils.formatDate(bench.run_at)) } ]; return `
${rows.map(row => `
${row.label}
${row.value}
`).join('')}
Chargement de l'historique...
`; } function renderTagsSection(device) { const tags = utils.parseTags(device.tags); const chips = tags.length ? `
${tags.map(tag => ` ${utils.escapeHtml(tag)} `).join('')}
` : '

Aucun tag configuré

'; return ` ${chips}
`; } function renderNotesSection(device) { if (editingNotes) { return ` `; } return utils.renderMarkdown(device.description || 'Aucune note enregistrée.'); } function renderPurchaseSection(device) { const infoRows = [ { label: 'Boutique', value: device.purchase_store || 'Non renseigné' }, { label: 'Date d\'achat', value: device.purchase_date || 'Non renseigné' }, { label: 'Prix', value: formatPriceValue(device.purchase_price) } ]; let infoHtml = ''; if (editingPurchase) { infoHtml = `
`; } else { infoHtml = `
${infoRows.map(row => `
${row.label}
${utils.escapeHtml(String(row.value))}
`).join('')}
`; } const docs = device.documents || []; const invoices = docs.filter(doc => ['invoice', 'warranty'].includes(doc.doc_type)); const docsHtml = invoices.length ? ` ` : '

Aucune facture ou garantie uploadée.

'; return infoHtml + docsHtml; } function renderUpgradeSection(bench) { if (!bench) { return '

Aucun benchmark disponible.

'; } if (editingUpgradeNotes) { return ` `; } if (bench.notes) { return utils.renderMarkdown(bench.notes); } return '

Aucune note d\'upgrade enregistrée sur le dernier benchmark.

'; } function renderLinksPlaceholder(deviceId) { return ` `; } function renderLinksList(links, deviceId) { if (!links || links.length === 0) { return '

Aucun lien enregistré.

'; } return ` `; } function renderEmptyDetailsPlaceholder() { return `
📊

Sélectionnez un device dans la liste pour afficher ses détails

`; } // Initialize devices page document.addEventListener('DOMContentLoaded', () => { loadDevices(); // Refresh every 30 seconds setInterval(loadDevices, 30000); }); // Preview image in modal function previewImage(docId, filename) { const downloadUrl = apiClient.getDocumentDownloadUrl(docId); const modalHtml = `
×
${utils.escapeHtml(filename)}
${utils.escapeHtml(filename)}
`; document.body.insertAdjacentHTML('beforeend', modalHtml); } function closeImagePreview() { const modal = document.getElementById('imagePreviewModal'); if (modal) { modal.remove(); } } // Preview PDF in modal function previewPDF(docId, filename) { const downloadUrl = apiClient.getDocumentDownloadUrl(docId); const modalHtml = `

📄 ${utils.escapeHtml(filename)}

`; document.body.insertAdjacentHTML('beforeend', modalHtml); } function closePDFPreview() { const modal = document.getElementById('pdfPreviewModal'); if (modal) { modal.remove(); } } // Make functions available globally for onclick handlers window.selectDevice = selectDevice; window.deleteDocument = deleteDocument; window.viewBenchmarkDetails = viewBenchmarkDetails; window.previewImage = previewImage; window.closeImagePreview = closeImagePreview; window.previewPDF = previewPDF; window.closePDFPreview = closePDFPreview; window.deleteDeviceFromList = deleteDeviceFromList; window.startNotesEdit = startNotesEdit; window.cancelNotesEdit = cancelNotesEdit; window.saveNotes = saveNotes; window.clearNotes = clearNotes; window.startUpgradeNotesEdit = startUpgradeNotesEdit; window.cancelUpgradeNotesEdit = cancelUpgradeNotesEdit; window.saveUpgradeNotes = saveUpgradeNotes; window.clearUpgradeNotes = clearUpgradeNotes; window.startPurchaseEdit = startPurchaseEdit; window.cancelPurchaseEdit = cancelPurchaseEdit; window.savePurchaseInfo = savePurchaseInfo; window.addTag = addTag; window.removeTag = removeTag; window.addDeviceLinkEntry = addDeviceLinkEntry; window.deleteDeviceLink = deleteDeviceLink; })(); // End of IIFE