2100 lines
85 KiB
JavaScript
Executable File
2100 lines
85 KiB
JavaScript
Executable File
// 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 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 '<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 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: `
|
||
<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 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 = '<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
|