maj
This commit is contained in:
@@ -1,17 +1,27 @@
|
||||
// Linux BenchTools - Devices Two-Panel Layout
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const { formatRelativeTime, createScoreBadge, getScoreBadgeText, escapeHtml, showError, showEmptyState, formatTags } = window.BenchUtils;
|
||||
const api = window.BenchAPI;
|
||||
// Use utilities from global scope directly - avoid redeclaration
|
||||
const utils = window.BenchUtils;
|
||||
const apiClient = window.BenchAPI;
|
||||
|
||||
let allDevices = [];
|
||||
let selectedDeviceId = null;
|
||||
let isEditing = false;
|
||||
let currentDevice = null;
|
||||
|
||||
// Load devices
|
||||
async function loadDevices() {
|
||||
const listContainer = document.getElementById('deviceList');
|
||||
|
||||
try {
|
||||
const data = await api.getDevices({ page_size: 1000 }); // Get all devices
|
||||
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 || [];
|
||||
|
||||
@@ -27,6 +37,7 @@ async function loadDevices() {
|
||||
return scoreB - scoreA;
|
||||
});
|
||||
|
||||
console.log('📋 Rendering', allDevices.length, 'devices');
|
||||
renderDeviceList();
|
||||
|
||||
// Auto-select first device if none selected
|
||||
@@ -35,8 +46,17 @@ async function loadDevices() {
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load devices:', error);
|
||||
listContainer.innerHTML = '<div style="padding: 1rem; color: var(--color-danger);">❌ Erreur de chargement</div>';
|
||||
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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +73,7 @@ function renderDeviceList() {
|
||||
: 'N/A';
|
||||
|
||||
const scoreClass = globalScore !== null && globalScore !== undefined
|
||||
? window.BenchUtils.getScoreBadgeClass(globalScore)
|
||||
? utils.getScoreBadgeClass(globalScore)
|
||||
: 'badge';
|
||||
|
||||
return `
|
||||
@@ -74,7 +94,7 @@ function renderDeviceList() {
|
||||
>
|
||||
<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);">
|
||||
${escapeHtml(device.hostname)}
|
||||
${utils.escapeHtml(device.hostname)}
|
||||
</div>
|
||||
<span class="${scoreClass}" style="font-size: 0.75rem; padding: 0.2rem 0.5rem;">
|
||||
${scoreText}
|
||||
@@ -82,7 +102,7 @@ function renderDeviceList() {
|
||||
</div>
|
||||
${device.last_benchmark?.run_at ? `
|
||||
<div style="font-size: 0.75rem; color: var(--text-secondary);">
|
||||
⏱️ ${formatRelativeTime(device.last_benchmark.run_at)}
|
||||
⏱️ ${utils.formatRelativeTime(device.last_benchmark.run_at)}
|
||||
</div>
|
||||
` : '<div style="font-size: 0.75rem; color: var(--color-warning);">⚠️ Pas de benchmark</div>'}
|
||||
</div>
|
||||
@@ -99,16 +119,233 @@ async function selectDevice(deviceId) {
|
||||
detailsContainer.innerHTML = '<div class="loading">Chargement des détails...</div>';
|
||||
|
||||
try {
|
||||
const device = await api.getDevice(deviceId);
|
||||
const device = await apiClient.getDevice(deviceId);
|
||||
renderDeviceDetails(device);
|
||||
} catch (error) {
|
||||
console.error('Failed to load device details:', error);
|
||||
showError(detailsContainer, 'Impossible de charger les détails du device.');
|
||||
utils.showError(detailsContainer, 'Impossible de charger les détails du device.');
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle edit mode
|
||||
function toggleEditMode() {
|
||||
isEditing = !isEditing;
|
||||
if (currentDevice) {
|
||||
renderDeviceDetails(currentDevice);
|
||||
}
|
||||
}
|
||||
|
||||
// Save device changes
|
||||
async function saveDevice() {
|
||||
if (!currentDevice) return;
|
||||
|
||||
const description = document.getElementById('edit-description')?.value || '';
|
||||
const location = document.getElementById('edit-location')?.value || '';
|
||||
const owner = document.getElementById('edit-owner')?.value || '';
|
||||
const assetTag = document.getElementById('edit-asset-tag')?.value || '';
|
||||
const tags = document.getElementById('edit-tags')?.value || '';
|
||||
|
||||
try {
|
||||
console.log('💾 Saving device changes...');
|
||||
|
||||
const updateData = {
|
||||
description: description || null,
|
||||
location: location || null,
|
||||
owner: owner || null,
|
||||
asset_tag: assetTag || null,
|
||||
tags: tags || null
|
||||
};
|
||||
|
||||
await apiClient.updateDevice(currentDevice.id, updateData);
|
||||
|
||||
console.log('✅ Device updated successfully');
|
||||
|
||||
// Reload device data
|
||||
isEditing = false;
|
||||
const updatedDevice = await apiClient.getDevice(currentDevice.id);
|
||||
currentDevice = updatedDevice;
|
||||
renderDeviceDetails(updatedDevice);
|
||||
|
||||
// Reload device list to reflect changes
|
||||
await loadDevices();
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to save device:', error);
|
||||
alert('Erreur lors de la sauvegarde: ' + (error.message || 'Erreur inconnue'));
|
||||
}
|
||||
}
|
||||
|
||||
// Upload image for device
|
||||
async function uploadImage() {
|
||||
if (!currentDevice) return;
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/*';
|
||||
|
||||
input.onchange = async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
// Check file size (max 10MB)
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
alert('L\'image est trop volumineuse (max 10MB)');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('📤 Uploading image:', file.name);
|
||||
|
||||
await apiClient.uploadDocument(currentDevice.id, file, 'image');
|
||||
|
||||
console.log('✅ Image uploaded successfully');
|
||||
|
||||
// Reload device data to show the new image
|
||||
const updatedDevice = await apiClient.getDevice(currentDevice.id);
|
||||
currentDevice = updatedDevice;
|
||||
renderDeviceDetails(updatedDevice);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to upload image:', error);
|
||||
alert('Erreur lors du chargement de l\'image: ' + (error.message || 'Erreur inconnue'));
|
||||
}
|
||||
};
|
||||
|
||||
input.click();
|
||||
}
|
||||
|
||||
// Upload PDF for device
|
||||
async function uploadPDF() {
|
||||
if (!currentDevice) return;
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'application/pdf';
|
||||
|
||||
input.onchange = async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
// Check file size (max 50MB)
|
||||
if (file.size > 50 * 1024 * 1024) {
|
||||
alert('Le PDF est trop volumineux (max 50MB)');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('📤 Uploading PDF:', file.name);
|
||||
|
||||
await apiClient.uploadDocument(currentDevice.id, file, 'manual');
|
||||
|
||||
console.log('✅ PDF uploaded successfully');
|
||||
|
||||
// Reload device data to show the new PDF
|
||||
const updatedDevice = await apiClient.getDevice(currentDevice.id);
|
||||
currentDevice = updatedDevice;
|
||||
renderDeviceDetails(updatedDevice);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to upload PDF:', error);
|
||||
alert('Erreur lors du chargement du PDF: ' + (error.message || 'Erreur inconnue'));
|
||||
}
|
||||
};
|
||||
|
||||
input.click();
|
||||
}
|
||||
|
||||
// Delete document
|
||||
async function deleteDocument(docId) {
|
||||
if (!currentDevice) return;
|
||||
|
||||
if (!confirm('Voulez-vous vraiment supprimer ce document ?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('🗑️ Deleting document:', docId);
|
||||
|
||||
await apiClient.deleteDocument(docId);
|
||||
|
||||
console.log('✅ Document deleted successfully');
|
||||
|
||||
// Reload device data
|
||||
const updatedDevice = await apiClient.getDevice(currentDevice.id);
|
||||
currentDevice = updatedDevice;
|
||||
renderDeviceDetails(updatedDevice);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to delete document:', error);
|
||||
alert('Erreur lors de la suppression: ' + (error.message || 'Erreur inconnue'));
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: Render image documents
|
||||
function renderImageDocuments(documents) {
|
||||
if (!documents || !Array.isArray(documents)) {
|
||||
return '<span>🖼️ Aucune image</span>';
|
||||
}
|
||||
|
||||
const imageDoc = documents.find(doc => doc.doc_type === 'image');
|
||||
|
||||
if (!imageDoc) {
|
||||
return '<span>🖼️ Aucune image</span>';
|
||||
}
|
||||
|
||||
const downloadUrl = apiClient.getDocumentDownloadUrl(imageDoc.id);
|
||||
|
||||
return `
|
||||
<div style="width: 100%; position: relative;">
|
||||
<img src="${downloadUrl}" alt="Device image" style="max-width: 100%; max-height: 300px; border-radius: 6px; object-fit: contain;">
|
||||
<div style="margin-top: 0.75rem; display: flex; justify-content: space-between; align-items: center;">
|
||||
<div style="font-size: 0.85rem; color: var(--text-secondary);">
|
||||
📎 ${utils.escapeHtml(imageDoc.filename)}
|
||||
</div>
|
||||
<button onclick="deleteDocument(${imageDoc.id})" class="btn btn-danger" style="padding: 0.3rem 0.6rem; font-size: 0.8rem;">🗑️ Supprimer</button>
|
||||
</div>
|
||||
</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 style="display: flex; gap: 0.5rem;">
|
||||
<a href="${downloadUrl}" download class="btn btn-primary" style="padding: 0.3rem 0.6rem; font-size: 0.8rem; text-decoration: none;">⬇️ Télécharger</a>
|
||||
<button onclick="deleteDocument(${doc.id})" class="btn btn-danger" style="padding: 0.3rem 0.6rem; font-size: 0.8rem;">🗑️ Supprimer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Render device details (right panel)
|
||||
function renderDeviceDetails(device) {
|
||||
currentDevice = device;
|
||||
const detailsContainer = document.getElementById('deviceDetailsContainer');
|
||||
const snapshot = device.last_hardware_snapshot;
|
||||
const bench = device.last_benchmark;
|
||||
@@ -148,7 +385,7 @@ function renderDeviceDetails(device) {
|
||||
const gpuScore = bench?.gpu_score;
|
||||
|
||||
const globalScoreHtml = globalScore !== null && globalScore !== undefined
|
||||
? `<span class="${window.BenchUtils.getScoreBadgeClass(globalScore)}" style="font-size: 1.5rem; padding: 0.5rem 1rem;">${getScoreBadgeText(globalScore)}</span>`
|
||||
? `<span class="${utils.getScoreBadgeClass(globalScore)}" style="font-size: 1.5rem; padding: 0.5rem 1rem;">${utils.getScoreBadgeText(globalScore)}</span>`
|
||||
: '<span class="badge">N/A</span>';
|
||||
|
||||
// Network details
|
||||
@@ -165,7 +402,7 @@ function renderDeviceDetails(device) {
|
||||
return `
|
||||
<div style="padding: 0.75rem; background: var(--bg-secondary); border-radius: 6px; margin-bottom: 0.5rem;">
|
||||
<div style="font-weight: 600; margin-bottom: 0.5rem;">
|
||||
${typeIcon} ${escapeHtml(iface.name)} (${iface.type})${wolBadge}
|
||||
${typeIcon} ${utils.escapeHtml(iface.name)} (${iface.type})${wolBadge}
|
||||
</div>
|
||||
<div style="font-size: 0.9rem; color: var(--text-secondary);">
|
||||
IP: ${iface.ip || 'N/A'} • MAC: ${iface.mac || 'N/A'}<br>
|
||||
@@ -222,76 +459,171 @@ function renderDeviceDetails(device) {
|
||||
}
|
||||
|
||||
detailsContainer.innerHTML = `
|
||||
<!-- Device Header -->
|
||||
<div style="border-bottom: 2px solid var(--border-color); padding-bottom: 1.5rem; margin-bottom: 1.5rem;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: start;">
|
||||
<div>
|
||||
<h2 style="margin: 0 0 0.5rem 0; font-size: 2rem;">${escapeHtml(device.hostname)}</h2>
|
||||
<p style="color: var(--text-secondary); margin: 0;">
|
||||
${escapeHtml(device.description || 'Aucune description')}
|
||||
<!-- Device Header with Action Buttons -->
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; padding-bottom: 0.75rem; border-bottom: 2px solid var(--color-primary);">
|
||||
<div style="flex: 1;">
|
||||
<h2 style="margin: 0; font-size: 1.5rem; color: var(--text-primary);">${utils.escapeHtml(device.hostname)}</h2>
|
||||
${isEditing ? `
|
||||
<textarea id="edit-description" style="width: 100%; margin-top: 0.5rem; padding: 0.5rem; border: 1px solid var(--border-color); border-radius: 4px; background: var(--bg-secondary); color: var(--text-primary); font-family: inherit; resize: vertical;" rows="2" placeholder="Description du device...">${device.description || ''}</textarea>
|
||||
` : `
|
||||
<p style="color: var(--text-secondary); margin: 0.25rem 0 0 0; font-size: 0.9rem;">
|
||||
${utils.escapeHtml(device.description || 'Aucune description')}
|
||||
</p>
|
||||
${device.location ? `<p style="color: var(--text-secondary); margin: 0.25rem 0 0 0;">📍 ${escapeHtml(device.location)}</p>` : ''}
|
||||
${device.tags ? `<div class="tags" style="margin-top: 0.5rem;">${formatTags(device.tags)}</div>` : ''}
|
||||
`}
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.75rem; align-items: center; margin-left: 1rem;">
|
||||
${globalScoreHtml}
|
||||
${!isEditing ? `
|
||||
<button id="btn-edit" class="btn btn-secondary" style="padding: 0.5rem 1rem; font-size: 0.9rem;">✏️ Edit</button>
|
||||
` : `
|
||||
<button id="btn-cancel" class="btn btn-secondary" style="padding: 0.5rem 1rem; font-size: 0.9rem;">✖️ Annuler</button>
|
||||
<button id="btn-save" class="btn btn-primary" style="padding: 0.5rem 1rem; font-size: 0.9rem;">💾 Save</button>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form-Style Layout -->
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem;">
|
||||
|
||||
<!-- Left Column: Caractéristiques -->
|
||||
<div style="background: var(--bg-secondary); padding: 1.5rem; border-radius: 8px; border: 1px solid var(--border-color);">
|
||||
<h3 style="margin: 0 0 1rem 0; font-size: 1.1rem; color: var(--color-primary); border-bottom: 1px solid var(--border-color); padding-bottom: 0.5rem;">Caractéristiques</h3>
|
||||
|
||||
${createFormRow('CPU', utils.escapeHtml(cpuModel))}
|
||||
${createFormRow('Cores / Threads', `${cpuCores} / ${cpuThreads}`)}
|
||||
${createFormRow('RAM Total', `${ramTotalGB} GB`)}
|
||||
${createFormRow('RAM Utilisée', ramUsedMB > 0 ? `${Math.round(ramUsedMB / 1024)} GB (${Math.round((ramUsedMB / (snapshot.ram_total_mb || 1)) * 100)}%)` : 'N/A')}
|
||||
${createFormRow('RAM Libre', ramFreeMB > 0 ? `${Math.round(ramFreeMB / 1024)} GB` : 'N/A')}
|
||||
${ramSharedMB > 0 ? createFormRow('RAM Partagée', `${Math.round(ramSharedMB / 1024)} GB`) : ''}
|
||||
${createFormRow('GPU', utils.escapeHtml(gpuSummary))}
|
||||
${createFormRow('Storage', utils.escapeHtml(storage))}
|
||||
${createFormRow('OS', utils.escapeHtml(osName))}
|
||||
${createFormRow('Kernel', utils.escapeHtml(kernelVersion))}
|
||||
${isEditing ? `
|
||||
<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;">Location</div>
|
||||
<input type="text" id="edit-location" value="${device.location || ''}" style="padding: 0.4rem; border: 1px solid var(--border-color); border-radius: 4px; background: var(--bg-primary); color: var(--text-primary); font-size: 0.9rem;" placeholder="Bureau, DataCenter, etc.">
|
||||
</div>
|
||||
<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;">Propriétaire</div>
|
||||
<input type="text" id="edit-owner" value="${device.owner || ''}" style="padding: 0.4rem; border: 1px solid var(--border-color); border-radius: 4px; background: var(--bg-primary); color: var(--text-primary); font-size: 0.9rem;" placeholder="Nom du propriétaire">
|
||||
</div>
|
||||
<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;">Asset Tag</div>
|
||||
<input type="text" id="edit-asset-tag" value="${device.asset_tag || ''}" style="padding: 0.4rem; border: 1px solid var(--border-color); border-radius: 4px; background: var(--bg-primary); color: var(--text-primary); font-size: 0.9rem;" placeholder="Numéro d'inventaire">
|
||||
</div>
|
||||
<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;">Tags</div>
|
||||
<input type="text" id="edit-tags" value="${device.tags || ''}" style="padding: 0.4rem; border: 1px solid var(--border-color); border-radius: 4px; background: var(--bg-primary); color: var(--text-primary); font-size: 0.9rem;" placeholder="production, test, dev (séparés par des virgules)">
|
||||
</div>
|
||||
` : `
|
||||
${device.location ? createFormRow('Location', utils.escapeHtml(device.location)) : ''}
|
||||
${device.owner ? createFormRow('Propriétaire', utils.escapeHtml(device.owner)) : ''}
|
||||
${device.asset_tag ? createFormRow('Asset Tag', utils.escapeHtml(device.asset_tag)) : ''}
|
||||
${device.tags ? createFormRow('Tags', utils.escapeHtml(device.tags)) : ''}
|
||||
`}
|
||||
${createFormRow('Créé le', new Date(device.created_at).toLocaleDateString('fr-FR', { year: 'numeric', month: 'long', day: 'numeric' }))}
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Image & PDF Sections -->
|
||||
<div style="display: flex; flex-direction: column; gap: 1.5rem;">
|
||||
|
||||
<!-- Image Section -->
|
||||
<div style="background: var(--bg-secondary); padding: 1.5rem; border-radius: 8px; border: 1px solid var(--border-color);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
||||
<h3 style="margin: 0; font-size: 1.1rem; color: var(--color-primary);">Image</h3>
|
||||
<button id="btn-upload-image" class="btn btn-secondary" style="padding: 0.4rem 0.8rem; font-size: 0.85rem;">📤 Upload</button>
|
||||
</div>
|
||||
<div id="image-container" style="background: var(--bg-primary); border: 2px dashed var(--border-color); border-radius: 6px; min-height: 180px; display: flex; flex-direction: column; align-items: center; justify-content: center; color: var(--text-secondary); padding: 1rem;">
|
||||
${renderImageDocuments(device.documents)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
${globalScoreHtml}
|
||||
|
||||
<!-- PDF Section -->
|
||||
<div style="background: var(--bg-secondary); padding: 1.5rem; border-radius: 8px; border: 1px solid var(--border-color);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
||||
<h3 style="margin: 0; font-size: 1.1rem; color: var(--color-primary);">Notice PDF</h3>
|
||||
<button id="btn-upload-pdf" class="btn btn-secondary" style="padding: 0.4rem 0.8rem; font-size: 0.85rem;">📤 Upload</button>
|
||||
</div>
|
||||
<div id="pdf-container" style="background: var(--bg-primary); border: 2px dashed var(--border-color); border-radius: 6px; min-height: 180px; display: flex; flex-direction: column; align-items: flex-start; justify-content: center; color: var(--text-secondary); padding: 1rem;">
|
||||
${renderPDFDocuments(device.documents)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Benchmark Scores -->
|
||||
<!-- Benchmark Scores Section -->
|
||||
${bench ? `
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h3 style="margin: 0 0 1rem 0; font-size: 1.3rem;">📊 Scores de Benchmark</h3>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem;">
|
||||
${createScoreCard(cpuScore, 'CPU', '🔧')}
|
||||
${createScoreCard(memScore, 'Mémoire', '💾')}
|
||||
${createScoreCard(diskScore, 'Disque', '💿')}
|
||||
${createScoreCard(netScore, 'Réseau', '🌐')}
|
||||
${createScoreCard(gpuScore, 'GPU', '🎮')}
|
||||
<div style="margin-top: 1.5rem; background: var(--bg-secondary); padding: 1.5rem; border-radius: 8px; border: 1px solid var(--border-color);">
|
||||
<h3 style="margin: 0 0 1rem 0; font-size: 1.1rem; color: var(--color-primary); border-bottom: 1px solid var(--border-color); padding-bottom: 0.5rem;">📊 Benchmark Scores</h3>
|
||||
<div style="display: grid; grid-template-columns: repeat(5, 1fr); gap: 1rem; margin-bottom: 1rem;">
|
||||
${createFormRow('CPU Score', cpuScore !== null && cpuScore !== undefined ? Math.round(cpuScore) : 'N/A', true)}
|
||||
${createFormRow('RAM Score', memScore !== null && memScore !== undefined ? Math.round(memScore) : 'N/A', true)}
|
||||
${createFormRow('Disk Score', diskScore !== null && diskScore !== undefined ? Math.round(diskScore) : 'N/A', true)}
|
||||
${createFormRow('Network Score', netScore !== null && netScore !== undefined ? Math.round(netScore) : 'N/A', true)}
|
||||
${createFormRow('GPU Score', gpuScore !== null && gpuScore !== undefined ? Math.round(gpuScore) : 'N/A', true)}
|
||||
</div>
|
||||
<div style="margin-top: 0.75rem; color: var(--text-secondary); font-size: 0.9rem;">
|
||||
⏱️ Dernier benchmark: ${bench.run_at ? formatRelativeTime(bench.run_at) : 'N/A'}
|
||||
<div style="padding: 0.75rem; background: var(--bg-primary); border-radius: 6px; text-align: center; color: var(--text-secondary); font-size: 0.9rem;">
|
||||
⏱️ Dernier benchmark: ${bench.run_at ? utils.formatRelativeTime(bench.run_at) : 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
` : '<div style="padding: 2rem; background: var(--bg-secondary); border-radius: 6px; text-align: center; color: var(--color-warning); margin-bottom: 2rem;">⚠️ Aucun benchmark disponible</div>'}
|
||||
` : ''}
|
||||
|
||||
<!-- Hardware Summary -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h3 style="margin: 0 0 1rem 0; font-size: 1.3rem;">🖥️ Résumé Matériel</h3>
|
||||
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem;">
|
||||
${createInfoCard('🔧 CPU', `${escapeHtml(cpuModel)}<br><small style="color: var(--text-secondary);">${cpuCores} cores / ${cpuThreads} threads</small>`)}
|
||||
${createInfoCard('💾 RAM', ramUsageHtml)}
|
||||
${createInfoCard('🎮 GPU', escapeHtml(gpuSummary))}
|
||||
${createInfoCard('💿 Storage', escapeHtml(storage))}
|
||||
${createInfoCard('🐧 OS', `${escapeHtml(osName)}<br><small style="color: var(--text-secondary);">Kernel: ${escapeHtml(kernelVersion)}</small>`)}
|
||||
${createInfoCard('⏰ Créé le', new Date(device.created_at).toLocaleDateString('fr-FR'))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Network Details -->
|
||||
<!-- Network Details Section -->
|
||||
${networkHtml || netBenchHtml ? `
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h3 style="margin: 0 0 1rem 0; font-size: 1.3rem;">🌐 Détails Réseau</h3>
|
||||
<div style="margin-top: 1.5rem; background: var(--bg-secondary); padding: 1.5rem; border-radius: 8px; border: 1px solid var(--border-color);">
|
||||
<h3 style="margin: 0 0 1rem 0; font-size: 1.1rem; color: var(--color-primary); border-bottom: 1px solid var(--border-color); padding-bottom: 0.5rem;">🌐 Détails Réseau</h3>
|
||||
${networkHtml}
|
||||
${netBenchHtml}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Actions -->
|
||||
<div style="margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid var(--border-color);">
|
||||
<a href="device_detail.html?id=${device.id}" class="btn btn-primary" style="text-decoration: none; display: inline-block;">
|
||||
📄 Voir la page complète
|
||||
<!-- Full Details Link -->
|
||||
<div style="margin-top: 1.5rem; text-align: center;">
|
||||
<a href="device_detail.html?id=${device.id}" class="btn btn-primary" style="text-decoration: none; display: inline-block; padding: 0.75rem 2rem;">
|
||||
📄 Voir la page complète avec tous les détails
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Attach event listeners for edit/save/upload buttons
|
||||
setTimeout(() => {
|
||||
const btnEdit = document.getElementById('btn-edit');
|
||||
const btnSave = document.getElementById('btn-save');
|
||||
const btnCancel = document.getElementById('btn-cancel');
|
||||
const btnUploadImage = document.getElementById('btn-upload-image');
|
||||
const btnUploadPDF = document.getElementById('btn-upload-pdf');
|
||||
|
||||
if (btnEdit) {
|
||||
btnEdit.addEventListener('click', toggleEditMode);
|
||||
}
|
||||
|
||||
if (btnSave) {
|
||||
btnSave.addEventListener('click', saveDevice);
|
||||
}
|
||||
|
||||
if (btnCancel) {
|
||||
btnCancel.addEventListener('click', () => {
|
||||
isEditing = false;
|
||||
renderDeviceDetails(currentDevice);
|
||||
});
|
||||
}
|
||||
|
||||
if (btnUploadImage) {
|
||||
btnUploadImage.addEventListener('click', uploadImage);
|
||||
}
|
||||
|
||||
if (btnUploadPDF) {
|
||||
btnUploadPDF.addEventListener('click', uploadPDF);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// Create score card for display
|
||||
function createScoreCard(score, label, icon) {
|
||||
const scoreValue = score !== null && score !== undefined ? Math.round(score) : 'N/A';
|
||||
const badgeClass = score !== null && score !== undefined
|
||||
? window.BenchUtils.getScoreBadgeClass(score)
|
||||
? utils.getScoreBadgeClass(score)
|
||||
: 'badge';
|
||||
|
||||
return `
|
||||
@@ -315,6 +647,25 @@ function createInfoCard(label, value) {
|
||||
`;
|
||||
}
|
||||
|
||||
// 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>
|
||||
`;
|
||||
}
|
||||
|
||||
// Initialize devices page
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadDevices();
|
||||
@@ -323,5 +674,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
setInterval(loadDevices, 30000);
|
||||
});
|
||||
|
||||
// Make selectDevice available globally
|
||||
// Make functions available globally for onclick handlers
|
||||
window.selectDevice = selectDevice;
|
||||
window.deleteDocument = deleteDocument;
|
||||
|
||||
})(); // End of IIFE
|
||||
|
||||
Reference in New Issue
Block a user