Files
serv_benchmark/frontend/js/devices.js
2026-01-11 23:41:30 +01:00

2100 lines
85 KiB
JavaScript
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 `<span class="section-icon" data-icon="${iconName}" title="${safeAlt}"></span>`;
}
// 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 = '<div style="padding: 1rem; text-align: center; color: var(--text-secondary);">📊<br>Aucun device</div>';
return;
}
// Sort by global_score descending
allDevices.sort((a, b) => {
const scoreA = a.last_benchmark?.global_score ?? -1;
const scoreB = b.last_benchmark?.global_score ?? -1;
return scoreB - scoreA;
});
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 = `
<div style="padding: 1rem; color: var(--color-danger); font-size: 0.85rem;">
<div style="font-weight: 600; margin-bottom: 0.5rem;">❌ Erreur</div>
<div>${error.message || 'Erreur de chargement'}</div>
<div style="margin-top: 0.5rem; font-size: 0.75rem;">
Backend: ${apiClient.baseURL}
</div>
</div>
`;
}
}
// Render device list (left panel)
function renderDeviceList() {
const listContainer = document.getElementById('deviceList');
listContainer.innerHTML = allDevices.map(device => {
const globalScore = device.last_benchmark?.global_score;
const isSelected = device.id === selectedDeviceId;
const hostnameEscaped = (device.hostname || '').replace(/'/g, "\\'").replace(/"/g, '\\"');
const scoreText = formatScoreValue(globalScore);
const scoreClass = globalScore !== null && globalScore !== undefined
? utils.getScoreBadgeClass(globalScore)
: 'badge';
return `
<div
class="device-list-item ${isSelected ? 'selected' : ''}"
onclick="selectDevice(${device.id})"
style="
padding: 0.75rem;
margin-bottom: 0.5rem;
background: ${isSelected ? 'var(--color-primary-alpha)' : 'var(--bg-secondary)'};
border: 1px solid ${isSelected ? 'var(--color-primary)' : 'var(--border-color)'};
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
"
onmouseover="if (!this.classList.contains('selected')) this.style.background='var(--bg-hover)'"
onmouseout="if (!this.classList.contains('selected')) this.style.background='var(--bg-secondary)'"
>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.25rem;">
<div style="font-weight: 600; font-size: 0.95rem; color: var(--text-primary);">
${utils.escapeHtml(device.hostname || 'N/A')}
</div>
<div style="display: flex; align-items: center; gap: 0.35rem;">
<span class="${scoreClass}" style="font-size: 0.75rem; padding: 0.2rem 0.5rem;">
${scoreText}
</span>
<button type="button" class="device-list-delete" title="Supprimer ce device" onclick="event.stopPropagation(); deleteDeviceFromList(event, ${device.id}, '${hostnameEscaped}')">🗑️</button>
</div>
</div>
${device.last_benchmark?.run_at ? `
<div style="font-size: 0.75rem; color: var(--text-secondary);">
⏱️ ${utils.formatRelativeTime(device.last_benchmark.run_at)}
</div>
` : '<div style="font-size: 0.75rem; color: var(--color-warning);">⚠️ Pas de benchmark</div>'}
</div>
`;
}).join('');
}
// Select device and display details
async function selectDevice(deviceId) {
selectedDeviceId = deviceId;
renderDeviceList(); // Update selection in list
const detailsContainer = document.getElementById('deviceDetailsContainer');
detailsContainer.innerHTML = '<div class="loading">Chargement des détails...</div>';
try {
const device = await 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 dachat 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 '<span>🖼️ Aucune image</span>';
}
const imageDocs = documents.filter(doc => doc.doc_type === 'image');
if (imageDocs.length === 0) {
return '<span>🖼️ Aucune image</span>';
}
return `
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 0.75rem; width: 100%;">
${imageDocs.map(imageDoc => {
const downloadUrl = apiClient.getDocumentDownloadUrl(imageDoc.id);
const safeFilenameArg = utils.escapeHtml(JSON.stringify(imageDoc.filename || ''));
return `
<div style="position: relative; border: 1px solid var(--border-color); border-radius: 6px; overflow: hidden; background: var(--bg-primary);">
<img
src="${downloadUrl}"
alt="${utils.escapeHtml(imageDoc.filename)}"
style="width: 100%; height: 150px; object-fit: cover; cursor: pointer;"
onclick="previewImage(${imageDoc.id}, ${safeFilenameArg})"
title="Prévisualiser"
>
<div style="padding: 0.5rem; display: flex; justify-content: space-between; align-items: center; background: var(--bg-secondary);">
<div style="font-size: 0.75rem; color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${utils.escapeHtml(imageDoc.filename)}">
📎 ${utils.escapeHtml(imageDoc.filename.substring(0, 15))}${imageDoc.filename.length > 15 ? '...' : ''}
</div>
<button onclick="event.stopPropagation(); deleteDocument(${imageDoc.id})" class="icon-btn danger" title="Supprimer" type="button">
<img src="icons/icons8-delete-48.png" alt="Supprimer">
</button>
</div>
</div>
`;
}).join('')}
</div>
`;
}
// Helper: Render PDF documents
function renderPDFDocuments(documents) {
if (!documents || !Array.isArray(documents)) {
return '<span>📄 Aucun PDF</span>';
}
const pdfDocs = documents.filter(doc => doc.doc_type === 'manual');
if (pdfDocs.length === 0) {
return '<span>📄 Aucun PDF</span>';
}
return pdfDocs.map(doc => {
const downloadUrl = apiClient.getDocumentDownloadUrl(doc.id);
const uploadDate = new Date(doc.uploaded_at).toLocaleDateString('fr-FR');
return `
<div style="width: 100%; background: var(--bg-secondary); padding: 0.75rem; border-radius: 6px; margin-bottom: 0.5rem; border: 1px solid var(--border-color);">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div style="flex: 1;">
<div style="font-weight: 600; color: var(--text-primary); margin-bottom: 0.25rem;">
📄 ${utils.escapeHtml(doc.filename)}
</div>
<div style="font-size: 0.8rem; color: var(--text-secondary);">
Uploadé le ${uploadDate}
</div>
</div>
<div class="doc-actions">
<button onclick="previewPDF(${doc.id}, ${utils.escapeHtml(JSON.stringify(doc.filename || ''))})" class="icon-btn" title="Prévisualiser" type="button">
<img src="icons/icons8-picture-48.png" alt="Preview">
</button>
<a href="${downloadUrl}" download class="icon-btn" title="Télécharger">
<img src="icons/icons8-save-48.png" alt="Télécharger">
</a>
<button onclick="deleteDocument(${doc.id})" class="icon-btn danger" title="Supprimer" type="button">
<img src="icons/icons8-delete-48.png" alt="Supprimer">
</button>
</div>
</div>
</div>
`;
}).join('');
}
// Helper: Render Motherboard Details
function renderMotherboardDetails(snapshot) {
if (!snapshot) {
return '<p style="color: var(--text-muted); text-align: center; padding: 2rem;">Aucune information disponible</p>';
}
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 `
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.75rem;">
${items.map(item => `
<div style="padding: 0.75rem; background: var(--bg-primary); border-radius: 6px; border: 1px solid var(--border-color);">
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 0.25rem;">${item.label}</div>
<div style="font-weight: 600; color: var(--text-primary);">${utils.escapeHtml(String(item.value))}</div>
</div>
`).join('')}
</div>
`;
}
// Helper: Render CPU Details
function renderCPUDetails(snapshot) {
if (!snapshot) {
return '<p style="color: var(--text-muted); text-align: center; padding: 2rem;">Aucune information disponible</p>';
}
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 = `
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 0.75rem;">
${items.map(item => `
<div style="padding: 0.75rem; background: var(--bg-primary); border-radius: 6px; border: 1px solid var(--border-color);" ${item.tooltip ? `class="tooltip" data-tooltip="${utils.escapeHtml(item.tooltip)}"` : ''}>
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 0.25rem;">${item.label}</div>
<div style="font-weight: 600; color: var(--text-primary); font-size: 0.95rem;">${utils.escapeHtml(String(item.value))}</div>
</div>
`).join('')}
</div>
`;
// 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 += `
<div style="margin-top: 1rem; padding: 1rem; background: var(--bg-primary); border-radius: 6px; border: 1px solid var(--border-color);">
<div style="font-weight: 600; margin-bottom: 0.5rem; color: var(--text-secondary);">Instructions supportées:</div>
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">
${flags.slice(0, 30).map(flag => `<span class="badge badge-muted" style="font-size: 0.75rem;">${utils.escapeHtml(flag)}</span>`).join('')}
${flags.length > 30 ? `<span class="badge">+${flags.length - 30} autres...</span>` : ''}
</div>
</div>
`;
}
} catch (e) {
console.warn('Failed to parse CPU flags:', e);
}
}
return html;
}
// Helper: Render Memory Details
function renderMemoryDetails(snapshot) {
if (!snapshot) {
return '<p style="color: var(--text-muted); text-align: center; padding: 2rem;">Aucune information disponible</p>';
}
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 = `
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 0.75rem;">
${items.map(item => `
<div style="padding: 0.75rem; background: var(--bg-primary); border-radius: 6px; border: 1px solid var(--border-color);">
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 0.25rem;">${item.label}</div>
<div style="font-weight: 600; color: var(--text-primary); font-size: 0.95rem;">${utils.escapeHtml(String(item.value))}</div>
</div>
`).join('')}
</div>
`;
// 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 += `
<div style="margin-top: 1rem; padding: 1rem; background: var(--bg-primary); border-radius: 6px; border: 1px solid var(--border-color);">
<div style="font-weight: 600; margin-bottom: 0.5rem; color: var(--text-secondary);">Configuration des barrettes:</div>
<div style="display: grid; gap: 0.5rem;">
${layout.map(slot => `
<div style="padding: 0.5rem; background: var(--bg-secondary); border-radius: 4px; font-size: 0.85rem;">
<strong>${utils.escapeHtml(slot.slot || 'N/A')}</strong>:
${utils.formatMemory(slot.size_mb || 0)}
${slot.type ? `(${utils.escapeHtml(slot.type)})` : ''}
${slot.speed_mhz ? `@ ${slot.speed_mhz} MHz` : ''}
</div>
`).join('')}
</div>
</div>
`;
}
} catch (e) {
console.warn('Failed to parse RAM layout:', e);
}
}
return html;
}
// Helper: Render Storage Details
function renderStorageDetails(snapshot) {
if (!snapshot) {
return '<p style="color: var(--text-muted); text-align: center; padding: 2rem;">Aucune information disponible</p>';
}
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 += `
<div style="display: grid; gap: 0.75rem;">
${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 `
<div style="border: 1px solid var(--border-color); border-radius: 8px; padding: 1rem; background: var(--bg-primary);">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 0.5rem;">
<div>
<div style="font-weight: 600; color: var(--color-primary);">
${typeIcon} ${utils.escapeHtml(disk.name || disk.device || 'N/A')}
</div>
<div style="color: var(--text-secondary); font-size: 0.85rem; margin-top: 0.25rem;">
${utils.escapeHtml(disk.model || 'Unknown model')}
</div>
</div>
${disk.smart_health ? `
<span class="badge" style="background: ${healthColor}; color: white; font-size: 0.75rem;">
${utils.escapeHtml(disk.smart_health)}
</span>
` : ''}
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 0.5rem; font-size: 0.85rem; color: var(--text-secondary);">
${disk.capacity_gb ? `<div><strong>Capacité:</strong> ${utils.formatStorage(disk.capacity_gb)}</div>` : ''}
${disk.type ? `<div><strong>Type:</strong> ${utils.escapeHtml(disk.type)}</div>` : ''}
${disk.interface ? `<div><strong>Interface:</strong> ${utils.escapeHtml(disk.interface)}</div>` : ''}
${disk.temperature_c ? `<div><strong>Température:</strong> ${utils.formatTemperature(disk.temperature_c)}</div>` : ''}
</div>
</div>
`;
}).join('')}
</div>
`;
} else {
html += '<p style="color: var(--text-muted); text-align: center; padding: 2rem;">Aucun disque détecté</p>';
}
}
} catch (e) {
console.error('Failed to parse storage devices:', e);
html += '<p style="color: var(--color-danger); text-align: center; padding: 2rem;">Erreur lors du parsing des données de stockage</p>';
}
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 += `
<div style="margin-top: 1.5rem;">
<h4 style="margin-bottom: 0.5rem; color: var(--text-secondary);">Partitions</h4>
<div style="overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="border-bottom: 1px solid var(--border-color); color: var(--text-secondary); text-align: left;">
<th style="padding: 0.5rem;">Partition</th>
<th style="padding: 0.5rem;">Montage</th>
<th style="padding: 0.5rem;">Type</th>
<th style="padding: 0.5rem;">Utilisé</th>
<th style="padding: 0.5rem;">Libre</th>
<th style="padding: 0.5rem;">Total</th>
<th style="padding: 0.5rem;">Usage</th>
</tr>
</thead>
<tbody>
${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 `
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 0.5rem; font-weight: 600;">${utils.escapeHtml(part.name || 'N/A')}</td>
<td style="padding: 0.5rem;">${part.mount_point ? utils.escapeHtml(part.mount_point) : '<span style="color: var(--text-muted);">Non monté</span>'}</td>
<td style="padding: 0.5rem;">${part.fs_type ? utils.escapeHtml(part.fs_type) : 'N/A'}</td>
<td style="padding: 0.5rem;">${used}</td>
<td style="padding: 0.5rem;">${free}</td>
<td style="padding: 0.5rem;">${total}</td>
<td style="padding: 0.5rem;">${renderUsageBadge(part.used_gb, part.total_gb)}</td>
</tr>
`;
}).join('')}
</tbody>
</table>
</div>
</div>
`;
}
} catch (error) {
console.error('Failed to parse partitions:', error);
html += '<p style="color: var(--color-danger); text-align: center; padding: 1rem;">Erreur lors de la lecture des partitions</p>';
}
}
if (!html) {
html = '<p style="color: var(--text-muted); text-align: center; padding: 2rem;">Aucune information disponible</p>';
}
return html;
}
// Helper: Render GPU Details
function renderGPUDetails(snapshot) {
if (!snapshot) {
return '<p style="color: var(--text-muted); text-align: center; padding: 2rem;">Aucune information disponible</p>';
}
if (!snapshot.gpu_vendor && !snapshot.gpu_model && !snapshot.gpu_summary) {
return '<p style="color: var(--text-muted); text-align: center; padding: 2rem;">Aucun GPU détecté</p>';
}
const items = [
{ label: 'Fabricant', value: snapshot.gpu_vendor || 'N/A' },
{ label: 'Modèle', value: snapshot.gpu_model || snapshot.gpu_summary || 'N/A' }
];
return `
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.75rem;">
${items.map(item => `
<div style="padding: 0.75rem; background: var(--bg-primary); border-radius: 6px; border: 1px solid var(--border-color);">
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 0.25rem;">${item.label}</div>
<div style="font-weight: 600; color: var(--text-primary);">${utils.escapeHtml(String(item.value))}</div>
</div>
`).join('')}
</div>
`;
}
// Helper: Render OS Details
function renderOSDetails(snapshot) {
if (!snapshot) {
return '<p style="color: var(--text-muted); text-align: center; padding: 2rem;">Aucune information disponible</p>';
}
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 `
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.75rem;">
${items.map(item => `
<div style="padding: 0.75rem; background: var(--bg-primary); border-radius: 6px; border: 1px solid var(--border-color);">
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 0.25rem;">${item.label}</div>
<div style="font-weight: 600; color: var(--text-primary);">${utils.escapeHtml(String(item.value))}</div>
</div>
`).join('')}
</div>
`;
}
// Helper: Render PCI Devices
function renderPCIDetails(snapshot) {
if (!snapshot || !snapshot.pci_devices_json) {
return '<p style="color: var(--text-muted); text-align: center; padding: 2rem;">Aucune information disponible</p>';
}
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 '<p style="color: var(--text-muted); text-align: center; padding: 2rem;">Aucun périphérique PCI détecté</p>';
}
return `
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.5rem; max-height: 400px; overflow-y: auto;">
${devices.map(dev => `
<div style="border: 1px solid var(--border-color); border-radius: 6px; padding: 0.75rem; background: var(--bg-primary);">
<div style="display: grid; grid-template-columns: 100px 1fr; gap: 0.5rem; font-size: 0.85rem;">
<div style="color: var(--text-secondary);">Slot:</div>
<div style="font-weight: 600; color: var(--color-primary);">${utils.escapeHtml(dev.slot || 'N/A')}</div>
<div style="color: var(--text-secondary);">Class:</div>
<div>${utils.escapeHtml(dev.class || 'N/A')}</div>
<div style="color: var(--text-secondary);">Vendor:</div>
<div>${utils.escapeHtml(dev.vendor || 'N/A')}</div>
<div style="color: var(--text-secondary);">Device:</div>
<div>${utils.escapeHtml(dev.device || 'N/A')}</div>
</div>
</div>
`).join('')}
</div>
`;
} catch (e) {
console.error('Failed to parse PCI devices:', e);
return '<p style="color: var(--color-danger); text-align: center; padding: 2rem;">Erreur lors du parsing des données PCI</p>';
}
}
// Helper: Render USB Devices
function renderUSBDetails(snapshot) {
if (!snapshot || !snapshot.usb_devices_json) {
return '<p style="color: var(--text-muted); text-align: center; padding: 2rem;">Aucune information disponible</p>';
}
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 '<p style="color: var(--text-muted); text-align: center; padding: 2rem;">Aucun périphérique USB détecté</p>';
}
return `
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.5rem; max-height: 400px; overflow-y: auto;">
${devices.map(dev => `
<div style="border: 1px solid var(--border-color); border-radius: 6px; padding: 0.75rem; background: var(--bg-primary);">
<div style="font-weight: 600; color: var(--color-primary); margin-bottom: 0.5rem;">
${utils.escapeHtml(dev.name || 'Unknown USB Device')}
</div>
<div style="display: grid; grid-template-columns: 120px 1fr; gap: 0.5rem; font-size: 0.85rem; color: var(--text-secondary);">
<div>Bus/Device:</div>
<div>${utils.escapeHtml(dev.bus || 'N/A')} / ${utils.escapeHtml(dev.device || 'N/A')}</div>
<div>Vendor ID:</div>
<div>${utils.escapeHtml(dev.vendor_id || 'N/A')}</div>
<div>Product ID:</div>
<div>${utils.escapeHtml(dev.product_id || 'N/A')}</div>
</div>
</div>
`).join('')}
</div>
`;
} catch (e) {
console.error('Failed to parse USB devices:', e);
return '<p style="color: var(--color-danger); text-align: center; padding: 2rem;">Erreur lors du parsing des données USB</p>';
}
}
// 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 = `
<div style="max-height: 500px; overflow-y: auto; background: var(--bg-primary); padding: 1rem; border-radius: 6px;">
<pre style="margin: 0; white-space: pre-wrap; word-wrap: break-word; font-size: 0.85rem; color: var(--text-primary);"><code>${JSON.stringify(benchmark.details || benchmark, null, 2)}</code></pre>
</div>
`;
} catch (error) {
console.error('Failed to load benchmark details:', error);
modalBody.innerHTML = `<div style="color: var(--color-danger); padding: 1rem;">Erreur: ${utils.escapeHtml(error.message)}</div>`;
}
}
// 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 `
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
<div style="display: flex; align-items: center; gap: 0.5rem;">
${ipUrl ? `<a href="${utils.escapeHtml(ipUrl)}" target="_blank" rel="noopener noreferrer" style="color: var(--color-info); text-decoration: none; font-weight: 600;" title="Ouvrir ${utils.escapeHtml(ipUrl)}">${utils.escapeHtml(displayIP)}</a>` : `<span>${utils.escapeHtml(displayIP)}</span>`}
<button id="btn-edit-ip-url" class="icon-btn" data-icon="edit" title="Éditer le lien IP" type="button" style="padding: 0.25rem; font-size: 0.75rem;">
<span data-icon="edit"></span>
</button>
</div>
<div id="ip-url-editor" style="display: none;">
<input type="text" id="ip-url-input" class="ip-url-input" placeholder="http://${ips[0] || '10.0.0.1'}" value="${utils.escapeHtml(ipUrl)}" style="width: 100%; padding: 0.5rem; border: 1px solid var(--border-color); border-radius: 4px; background: var(--bg-primary); color: var(--text-primary);">
<div style="display: flex; gap: 0.5rem; margin-top: 0.5rem;">
<button id="btn-save-ip-url" class="btn btn-success btn-sm" data-icon="check" type="button">
<span data-icon="check"></span> Sauvegarder
</button>
<button id="btn-cancel-ip-url" class="btn btn-secondary btn-sm" data-icon="times" type="button">
<span data-icon="times"></span> Annuler
</button>
</div>
</div>
</div>
`;
}
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 = `
<div class="device-preamble">
<div class="preamble-content">
<div class="preamble-left">
<div class="header-row">
<div>
<div class="header-label">Hostname</div>
<div class="header-value">${utils.escapeHtml(device.hostname)}</div>
<div class="header-meta">${metaParts.length > 0 ? metaParts.join(' • ') : 'Aucune métadonnée'}</div>
</div>
</div>
<div class="header-row">
<div>
<div class="header-label">Adresse IP</div>
<div class="header-value" id="ip-display-container">
${renderIPDisplay(snapshot, device)}
</div>
</div>
</div>
<div class="header-row">
<div>
<div class="header-label">Marque</div>
<div class="header-value">${utils.escapeHtml(brand)}</div>
</div>
</div>
<div class="header-row">
<div>
<div class="header-label">Modèle</div>
<div class="header-value" style="display: flex; align-items: center; gap: 0.5rem;">
<span>${utils.escapeHtml(model)}</span>
<button id="btn-search-model" class="icon-btn" title="Recherche sur le Web" type="button" style="padding: 0.25rem; font-size: 0.75rem;" data-model="${utils.escapeHtml(model)}">
<span data-icon="globe"></span>
</button>
</div>
</div>
</div>
<div class="header-row">
<div class="header-stat">
<div class="header-label">Score global</div>
<div class="header-value">${formatScoreValue(bench?.global_score)}</div>
</div>
</div>
<div class="header-row">
<button id="btn-delete" class="icon-btn danger" title="Supprimer ce device" type="button">
<img src="icons/icons8-delete-48.png" alt="Supprimer">
</button>
</div>
</div>
<div class="preamble-right">
${createSection(
'section-images-header',
getSectionIcon('images', 'Images'),
'Images',
imagesContent,
{
actionsHtml: `
<button id="btn-upload-image-header" class="icon-btn" title="Ajouter une image" type="button">
<img src="icons/icons8-picture-48.png" alt="Ajouter">
</button>
`
}
)}
</div>
</div>
</div>
`;
const metadataActions = isEditing
? `
<button id="btn-save" class="icon-btn success" title="Sauvegarder" type="button">
<img src="icons/icons8-save-48.png" alt="Sauvegarder">
</button>
<button id="btn-cancel" class="icon-btn" title="Annuler" type="button">
<img src="icons/icons8-close-48.png" alt="Annuler">
</button>
`
: `
<button id="btn-edit" class="icon-btn" title="Éditer les métadonnées" type="button">
<img src="icons/icons8-edit-pencil-48.png" alt="Éditer">
</button>
`;
const notesActions = editingNotes
? `
<button class="icon-btn success" title="Sauvegarder les notes" onclick="saveNotes()" type="button">
<img src="icons/icons8-save-48.png" alt="Sauvegarder">
</button>
<button class="icon-btn" title="Annuler" onclick="cancelNotesEdit()" type="button">
<img src="icons/icons8-close-48.png" alt="Annuler">
</button>
`
: `
<button class="icon-btn" title="Éditer les notes" onclick="startNotesEdit()" type="button">
<img src="icons/icons8-edit-pencil-48.png" alt="Éditer les notes">
</button>
${device.description ? `
<button class="icon-btn danger" title="Supprimer les notes" onclick="clearNotes()" type="button">
<img src="icons/icons8-delete-48.png" alt="Supprimer">
</button>
` : ''}
`;
const upgradeActions = currentDevice?.last_benchmark
? (editingUpgradeNotes
? `
<button class="icon-btn success" title="Sauvegarder" onclick="saveUpgradeNotes()" type="button">
<img src="icons/icons8-save-48.png" alt="Sauvegarder">
</button>
<button class="icon-btn" title="Annuler" onclick="cancelUpgradeNotesEdit()" type="button">
<img src="icons/icons8-close-48.png" alt="Annuler">
</button>
`
: `
<button class="icon-btn" title="Éditer les upgrade notes" onclick="startUpgradeNotesEdit()" type="button">
<img src="icons/icons8-edit-pencil-48.png" alt="Éditer">
</button>
${currentDevice.last_benchmark.notes ? `
<button class="icon-btn danger" title="Supprimer" onclick="clearUpgradeNotes()" type="button">
<img src="icons/icons8-delete-48.png" alt="Supprimer">
</button>
` : ''}
`)
: '';
const purchaseActions = editingPurchase
? `
<button class="icon-btn success" title="Sauvegarder" onclick="savePurchaseInfo()" type="button">
<img src="icons/icons8-save-48.png" alt="Sauvegarder">
</button>
<button class="icon-btn" title="Annuler" onclick="cancelPurchaseEdit()" type="button">
<img src="icons/icons8-close-48.png" alt="Annuler">
</button>
`
: `
<button class="icon-btn" title="Modifier les informations d'achat" onclick="startPurchaseEdit()" type="button">
<img src="icons/icons8-edit-pencil-48.png" alt="Éditer">
</button>
`;
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 dexploitation', 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: `
<button id="btn-upload-pdf" class="icon-btn" title="Ajouter un PDF" type="button">
<img src="icons/icons8-bios-94.png" alt="Ajouter un PDF">
</button>
`
}
);
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 dachat'), 'Informations dachat', 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 = '<div class="loading">Chargement des liens...</div>';
try {
const links = await apiClient.getDeviceLinks(deviceId);
listContainer.innerHTML = renderLinksList(links, deviceId);
} catch (error) {
console.error('Failed to load links:', error);
listContainer.innerHTML = `<p style="color: var(--color-danger);">Erreur: ${utils.escapeHtml(error.message || 'Impossible de charger les liens')}</p>`;
}
}
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 = '<p style="color: var(--text-secondary);">Aucun benchmark dans l\'historique.</p>';
return;
}
container.innerHTML = `
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Date</th>
<th>Global</th>
<th>CPU</th>
<th>CPU Mono</th>
<th>CPU Multi</th>
<th>Mémoire</th>
<th>Disque</th>
<th>Réseau</th>
<th>GPU</th>
<th>Version</th>
</tr>
</thead>
<tbody>
${data.items.map(bench => `
<tr>
<td>${utils.escapeHtml(utils.formatDate(bench.run_at))}</td>
<td>${formatScoreValue(bench.global_score)}</td>
<td>${formatScoreValue(bench.cpu_score)}</td>
<td>${formatScoreValue(bench.cpu_score_single)}</td>
<td>${formatScoreValue(bench.cpu_score_multi)}</td>
<td>${formatScoreValue(bench.memory_score)}</td>
<td>${formatScoreValue(bench.disk_score)}</td>
<td>${formatScoreValue(bench.network_score)}</td>
<td>${formatScoreValue(bench.gpu_score)}</td>
<td>${utils.escapeHtml(bench.bench_script_version || 'N/A')}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
} catch (error) {
console.error('Failed to load benchmark history:', error);
container.innerHTML = `<p style="color: var(--color-danger);">Erreur: ${utils.escapeHtml(error.message || 'Impossible de charger l\'historique')}</p>`;
}
}
// 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 `
<div style="background: var(--bg-secondary); padding: 1rem; border-radius: 6px; text-align: center;">
<div style="font-size: 1.5rem; margin-bottom: 0.25rem;">${icon}</div>
<div style="font-size: 0.9rem; color: var(--text-secondary); margin-bottom: 0.5rem;">${label}</div>
<div class="${badgeClass}" style="font-size: 1.25rem; padding: 0.25rem 0.75rem; display: inline-block;">
${scoreValue}
</div>
</div>
`;
}
// Create info card
function createInfoCard(label, value) {
return `
<div style="background: var(--bg-secondary); padding: 1rem; border-radius: 6px;">
<div style="font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);">${label}</div>
<div style="color: var(--text-secondary);">${value}</div>
</div>
`;
}
// Create form row (label: value)
function createFormRow(label, value, inline = false) {
if (inline) {
return `
<div style="text-align: center;">
<div style="font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 0.25rem;">${label}</div>
<div style="font-weight: 600; color: var(--text-primary); font-size: 1.1rem;">${value}</div>
</div>
`;
}
return `
<div style="display: grid; grid-template-columns: 140px 1fr; gap: 1rem; padding: 0.5rem 0; border-bottom: 1px solid var(--border-color);">
<div style="font-weight: 500; color: var(--text-secondary); font-size: 0.9rem;">${label}</div>
<div style="color: var(--text-primary); font-size: 0.9rem;">${value}</div>
</div>
`;
}
function createSection(id, icon, title, content, options = {}) {
const iconHtml = icon ? `<span class="section-icon-wrap">${icon}</span>` : '';
const actionsHtml = options.actionsHtml
? `<div class="section-actions">${options.actionsHtml}</div>`
: '';
return `
<section class="device-section" id="${id}">
<div class="section-header">
<h3>${iconHtml}<span class="section-title">${title}</span></h3>
${actionsHtml}
</div>
<div class="section-body">
${content}
</div>
</section>
`;
}
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 '<span class="usage-pill muted">N/A</span>';
}
const percent = Math.min(100, Math.max(0, Math.round((used / total) * 100)));
const modifier = percent >= 85 ? 'high' : percent >= 60 ? 'medium' : 'ok';
return `<span class="usage-pill ${modifier}">${percent}%</span>`;
}
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 `
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 0.75rem;">
<div style="width: 140px; font-weight: 500; color: var(--text-secondary); font-size: 0.85rem;">${label}</div>
<input
type="text"
id="${id}"
value="${utils.escapeHtml(value || '')}"
placeholder="${utils.escapeHtml(placeholder)}"
style="flex: 1; padding: 0.4rem 0.6rem; border: 1px solid var(--border-color); border-radius: 4px; background: var(--bg-primary); color: var(--text-primary); font-size: 0.9rem;"
>
</div>
`;
}
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 '<p style="color: var(--text-secondary);">Aucune information réseau disponible</p>';
}
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 '<p style="color: var(--text-secondary);">Aucune interface réseau détectée</p>';
}
const hostLabel = snapshot.hostname || currentDevice?.hostname || null;
html += '<div style="display: grid; gap: 1rem;">';
interfaces.forEach(iface => {
const typeIcon = iface.type === 'ethernet' ? '🔌' : (iface.type === 'wifi' ? '📡' : '🌐');
const wol = iface.wake_on_lan;
const wolBadge = wol === true
? '<span class="badge badge-success" style="margin-left: 0.5rem;">WoL ✓</span>'
: (wol === false ? '<span class="badge badge-muted" style="margin-left: 0.5rem;">WoL ✗</span>' : '');
const networkName = iface.network_name || iface.connection_name || iface.profile || iface.ssid || null;
html += `
<div style="border: 1px solid var(--border-color); border-radius: 8px; padding: 1rem;">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 0.75rem;">
<div>
<div style="font-weight: 600; color: var(--color-primary); font-size: 1.05rem;">${typeIcon} ${utils.escapeHtml(iface.name || 'N/A')}</div>
<div style="color: var(--text-secondary); font-size: 0.9rem; margin-top: 0.25rem;">${utils.escapeHtml(iface.type || 'unknown')}</div>
</div>
<div>${wolBadge}</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 0.75rem; font-size: 0.9rem;">
${hostLabel ? `<div><strong style="color: var(--text-secondary);">Hostname:</strong><br>${utils.escapeHtml(hostLabel)}</div>` : ''}
${networkName ? `<div><strong style="color: var(--text-secondary);">Nom du réseau:</strong><br>${utils.escapeHtml(networkName)}</div>` : ''}
${iface.ip ? `<div><strong style="color: var(--text-secondary);">Adresse IP:</strong><br><code>${utils.escapeHtml(iface.ip)}</code></div>` : ''}
${iface.mac ? `<div><strong style="color: var(--text-secondary);">MAC:</strong><br><code>${utils.escapeHtml(iface.mac)}</code></div>` : ''}
${iface.speed_mbps ? `<div><strong style="color: var(--text-secondary);">Vitesse:</strong><br>${iface.speed_mbps} Mbps</div>` : ''}
${iface.driver ? `<div><strong style="color: var(--text-secondary);">Driver:</strong><br>${utils.escapeHtml(iface.driver)}</div>` : ''}
${iface.ssid ? `<div><strong style="color: var(--text-secondary);">SSID:</strong><br>${utils.escapeHtml(iface.ssid)}</div>` : ''}
</div>
</div>
`;
});
html += '</div>';
} catch (error) {
console.error('Failed to parse network interfaces:', error);
html = '<p style="color: var(--color-danger);">Erreur lors du parsing des données réseau</p>';
}
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 += `
<div style="margin-top: 1rem; padding: 1rem; background: var(--bg-primary); border-radius: 6px; border: 1px solid var(--border-color);">
<div style="font-weight: 600; margin-bottom: 0.75rem; color: var(--text-secondary);">📈 Résultats iperf3</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 1rem; text-align: center;">
<div>
<div style="color: var(--color-success); font-size: 1.5rem; font-weight: 600;">${formatRawValue(netResults.upload_mbps)}</div>
<div style="font-size: 0.8rem; color: var(--text-secondary);">Upload Mbps</div>
</div>
<div>
<div style="color: var(--color-info); font-size: 1.5rem; font-weight: 600;">${formatRawValue(netResults.download_mbps)}</div>
<div style="font-size: 0.8rem; color: var(--text-secondary);">Download Mbps</div>
</div>
<div>
<div style="color: var(--color-warning); font-size: 1.5rem; font-weight: 600;">${formatRawValue(netResults.ping_ms)}</div>
<div style="font-size: 0.8rem; color: var(--text-secondary);">Ping ms</div>
</div>
<div>
<div style="color: var(--color-purple); font-size: 1.5rem; font-weight: 600;">${formatScoreValue(netResults.score)}</div>
<div style="font-size: 0.8rem; color: var(--text-secondary);">Score</div>
</div>
</div>
</div>
`;
} catch (error) {
console.error('Failed to parse network benchmark results:', error);
}
}
return html;
}
function renderNetworkSharesDetails(snapshot) {
if (!snapshot || !snapshot.network_shares_json) {
return '<p style="color: var(--text-secondary);">Aucun partage réseau détecté</p>';
}
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 '<p style="color: var(--text-secondary);">Aucun partage réseau monté</p>';
}
return `
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Source</th>
<th>Montage</th>
<th>Protocole</th>
<th>Type</th>
<th>Utilisé</th>
<th>Libre</th>
<th>Total</th>
<th>Options</th>
</tr>
</thead>
<tbody>
${shares.map(share => `
<tr>
<td>${utils.escapeHtml(share.source || 'N/A')}</td>
<td>${utils.escapeHtml(share.mount_point || 'N/A')}</td>
<td>${utils.escapeHtml(share.protocol || share.fs_type || 'N/A')}</td>
<td>${share.fs_type ? utils.escapeHtml(share.fs_type) : 'N/A'}</td>
<td>${typeof share.used_gb === 'number' ? utils.formatStorage(share.used_gb, 'GB') : 'N/A'}</td>
<td>${typeof share.free_gb === 'number' ? utils.formatStorage(share.free_gb, 'GB') : 'N/A'}</td>
<td>${typeof share.total_gb === 'number' ? utils.formatStorage(share.total_gb, 'GB') : 'N/A'}</td>
<td>${share.options ? utils.escapeHtml(share.options) : 'N/A'}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
} catch (error) {
console.error('Failed to parse network shares:', error);
return '<p style="color: var(--color-danger);">Erreur lors de la lecture des partages réseau</p>';
}
}
function renderBenchmarkSection(deviceId, bench) {
if (!bench) {
return '<p style="color: var(--text-secondary);">Aucun benchmark disponible pour ce device.</p>';
}
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 `
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 0.75rem;">
${rows.map(row => `
<div style="padding: 0.75rem; background: var(--bg-primary); border-radius: 6px; border: 1px solid var(--border-color);">
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 0.25rem;">${row.label}</div>
<div style="font-weight: 600; font-size: 1rem;">${row.value}</div>
</div>
`).join('')}
</div>
<div style="margin-top: 1rem; display: flex; gap: 0.75rem; flex-wrap: wrap;">
<button class="btn btn-secondary btn-sm" onclick="viewBenchmarkDetails(${bench.id})">📋 Voir les détails JSON</button>
</div>
<div id="benchmark-history-${deviceId}" style="margin-top: 1.5rem;">
<div class="loading">Chargement de l'historique...</div>
</div>
`;
}
function renderTagsSection(device) {
const tags = utils.parseTags(device.tags);
const chips = tags.length
? `
<div class="tags">
${tags.map(tag => `
<span class="tag tag-primary">
${utils.escapeHtml(tag)}
<button type="button" class="remove-tag" title="Supprimer ${utils.escapeHtml(tag)}" onclick="removeTag(${utils.escapeHtml(JSON.stringify(tag))})">×</button>
</span>
`).join('')}
</div>
`
: '<p style="color: var(--text-secondary);">Aucun tag configuré</p>';
return `
${chips}
<div class="tag-form">
<input
type="text"
id="new-tag-input-${device.id}"
placeholder="Ajouter un tag"
>
<button class="icon-btn" title="Ajouter un tag" onclick="addTag()" type="button">
<img src="icons/icons8-done-48.png" alt="Ajouter un tag">
</button>
</div>
`;
}
function renderNotesSection(device) {
if (editingNotes) {
return `
<textarea
id="notes-editor"
rows="6"
style="width: 100%; padding: 0.75rem; border: 1px solid var(--border-color); border-radius: 6px; background: var(--bg-primary); color: var(--text-primary); font-family: inherit; resize: vertical;"
placeholder="Notes techniques, incidents, TODO..."
>${utils.escapeHtml(device.description || '')}</textarea>
`;
}
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 = `
<div class="inline-form">
<input id="purchase-store-input" type="text" placeholder="Boutique / Revendeur" value="${utils.escapeHtml(device.purchase_store || '')}">
<input id="purchase-date-input" type="date" value="${device.purchase_date || ''}">
<input id="purchase-price-input" type="number" step="0.01" placeholder="Prix en €" value="${device.purchase_price != null ? device.purchase_price : ''}">
</div>
`;
} else {
infoHtml = `
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.75rem;">
${infoRows.map(row => `
<div style="padding: 0.75rem; background: var(--bg-primary); border-radius: 6px; border: 1px solid var(--border-color);">
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 0.25rem;">${row.label}</div>
<div style="font-weight: 600;">${utils.escapeHtml(String(row.value))}</div>
</div>
`).join('')}
</div>
`;
}
const docs = device.documents || [];
const invoices = docs.filter(doc => ['invoice', 'warranty'].includes(doc.doc_type));
const docsHtml = invoices.length
? `
<ul style="list-style: none; padding-left: 0; display: grid; gap: 0.5rem; margin-top: 1rem;">
${invoices.map(doc => `
<li style="border: 1px solid var(--border-color); border-radius: 6px; padding: 0.75rem; background: var(--bg-primary);">
<div style="font-weight: 600; color: var(--text-primary);">${doc.doc_type === 'invoice' ? 'Facture' : 'Garantie'}${utils.escapeHtml(doc.filename)}</div>
<div style="font-size: 0.85rem; color: var(--text-secondary);">Uploadé le ${utils.formatDate(doc.uploaded_at)}${utils.formatFileSize(doc.size_bytes)}</div>
</li>
`).join('')}
</ul>
`
: '<p style="color: var(--text-secondary); margin-top: 1rem;">Aucune facture ou garantie uploadée.</p>';
return infoHtml + docsHtml;
}
function renderUpgradeSection(bench) {
if (!bench) {
return '<p style="color: var(--text-secondary);">Aucun benchmark disponible.</p>';
}
if (editingUpgradeNotes) {
return `
<textarea
id="upgrade-notes-editor"
rows="6"
style="width: 100%; padding: 0.75rem; border: 1px solid var(--border-color); border-radius: 6px; background: var(--bg-primary); color: var(--text-primary); font-family: inherit; resize: vertical;"
placeholder="Consignez les upgrades à prévoir, pièces remplacées, etc."
>${utils.escapeHtml(bench.notes || '')}</textarea>
`;
}
if (bench.notes) {
return utils.renderMarkdown(bench.notes);
}
return '<p style="color: var(--text-secondary);">Aucune note d\'upgrade enregistrée sur le dernier benchmark.</p>';
}
function renderLinksPlaceholder(deviceId) {
return `
<div class="links-form">
<input
type="text"
id="link-label-${deviceId}"
placeholder="Libellé (ex: Support, Datasheet)"
style="flex: 1;"
>
<input
type="url"
id="link-url-${deviceId}"
placeholder="https://..."
style="flex: 1;"
>
<button class="icon-btn" title="Ajouter un lien" onclick="addDeviceLinkEntry(${deviceId})" type="button">
<img src="icons/icons8-done-48.png" alt="Ajouter">
</button>
</div>
<div id="links-list-${deviceId}" class="loading">Chargement des liens...</div>
`;
}
function renderLinksList(links, deviceId) {
if (!links || links.length === 0) {
return '<p style="color: var(--text-secondary);">Aucun lien enregistré.</p>';
}
return `
<ul style="list-style: none; padding-left: 0; display: grid; gap: 0.5rem;">
${links.map(link => `
<li style="border: 1px solid var(--border-color); border-radius: 6px; padding: 0.75rem; background: var(--bg-primary); display: flex; justify-content: space-between; align-items: center; gap: 0.5rem;">
<div>
<div style="font-weight: 600;">${utils.escapeHtml(link.label)}</div>
<div style="font-size: 0.85rem;"><a href="${utils.escapeHtml(link.url)}" target="_blank" rel="noopener">${utils.escapeHtml(link.url)}</a></div>
</div>
<button class="icon-btn danger" title="Supprimer le lien" onclick="deleteDeviceLink(${link.id}, ${deviceId})" type="button">
<img src="icons/icons8-delete-48.png" alt="Supprimer">
</button>
</li>
`).join('')}
</ul>
`;
}
function renderEmptyDetailsPlaceholder() {
return `
<div style="text-align: center; color: var(--text-secondary); padding: 4rem 2rem;">
<div style="font-size: 3rem; margin-bottom: 1rem;">📊</div>
<p style="font-size: 1.1rem;">Sélectionnez un device dans la liste pour afficher ses détails</p>
</div>
`;
}
// 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 = `
<div id="imagePreviewModal" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); z-index: 10000; display: flex; align-items: center; justify-content: center; flex-direction: column;" onclick="closeImagePreview()">
<div style="position: absolute; top: 20px; right: 20px; color: white; font-size: 2rem; cursor: pointer; z-index: 10001;" onclick="closeImagePreview()">×</div>
<img src="${downloadUrl}" alt="${utils.escapeHtml(filename)}" style="max-width: 90%; max-height: 90vh; object-fit: contain;" onclick="event.stopPropagation()">
<div style="color: white; margin-top: 1rem; font-size: 1.1rem;">${utils.escapeHtml(filename)}</div>
</div>
`;
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 = `
<div id="pdfPreviewModal" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); z-index: 10000; display: flex; align-items: center; justify-content: center; flex-direction: column;">
<div style="width: 90%; height: 90vh; background: white; border-radius: 8px; display: flex; flex-direction: column;">
<div style="display: flex; justify-content: space-between; align-items: center; padding: 1rem; background: var(--bg-secondary); border-bottom: 2px solid var(--border-color); border-radius: 8px 8px 0 0;">
<h3 style="margin: 0; color: var(--text-primary);">📄 ${utils.escapeHtml(filename)}</h3>
<button onclick="closePDFPreview()" style="background: var(--color-danger); color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; font-size: 1rem;">✖ Fermer</button>
</div>
<iframe src="${downloadUrl}" style="flex: 1; border: none; width: 100%;"></iframe>
</div>
</div>
`;
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