// 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.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
| Partition |
Montage |
Type |
Utilisé |
Libre |
Total |
Usage |
${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 `
| ${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)} |
`;
}).join('')}
`;
}
} 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 `
`;
}
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 = `
${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 = `
| Date |
Global |
CPU |
CPU Mono |
CPU Multi |
Mémoire |
Disque |
Réseau |
GPU |
Version |
${data.items.map(bench => `
| ${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')} |
`).join('')}
`;
} 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 `
`;
}
// Create form row (label: value)
function createFormRow(label, value, inline = false) {
if (inline) {
return `
`;
}
return `
`;
}
function createSection(id, icon, title, content, options = {}) {
const iconHtml = icon ? `${icon}` : '';
const actionsHtml = options.actionsHtml
? `${options.actionsHtml}
`
: '';
return `
`;
}
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 `
`;
}
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 `
| Source |
Montage |
Protocole |
Type |
Utilisé |
Libre |
Total |
Options |
${shares.map(share => `
| ${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'} |
`).join('')}
`;
} 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 `
Chargement des liens...
`;
}
function renderLinksList(links, deviceId) {
if (!links || links.length === 0) {
return 'Aucun lien enregistré.
';
}
return `
${links.map(link => `
-
${utils.escapeHtml(link.label)}
`).join('')}
`;
}
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)}
`;
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