maj
This commit is contained in:
@@ -22,7 +22,16 @@ class BenchAPI {
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `HTTP ${response.status}: ${response.statusText}`);
|
||||
const errorMessage = errorData.detail || errorData.message || `HTTP ${response.status}: ${response.statusText}`;
|
||||
|
||||
// Create a detailed error object
|
||||
const apiError = new Error(errorMessage);
|
||||
apiError.status = response.status;
|
||||
apiError.statusText = response.statusText;
|
||||
apiError.endpoint = endpoint;
|
||||
apiError.response = errorData;
|
||||
|
||||
throw apiError;
|
||||
}
|
||||
|
||||
// Handle 204 No Content
|
||||
@@ -30,9 +39,21 @@ class BenchAPI {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
// Return text for non-JSON responses
|
||||
return await response.text();
|
||||
} catch (error) {
|
||||
console.error(`API Error [${endpoint}]:`, error);
|
||||
|
||||
// Handle network errors
|
||||
if (error.name === 'TypeError' && error.message === 'Failed to fetch') {
|
||||
error.message = 'Impossible de se connecter au serveur backend. Vérifiez que le service est démarré.';
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,60 @@
|
||||
// Linux BenchTools - Dashboard Logic
|
||||
|
||||
const { formatDate, formatRelativeTime, createScoreBadge, getScoreBadgeText, escapeHtml, showError, showEmptyState, copyToClipboard, showToast } = window.BenchUtils;
|
||||
const { formatDate, formatRelativeTime, createScoreBadge, getScoreBadgeText, escapeHtml, showError, showEmptyState, copyToClipboard, showToast, debounce } = window.BenchUtils;
|
||||
const api = window.BenchAPI;
|
||||
|
||||
// Global state
|
||||
let allDevices = [];
|
||||
let isLoading = false;
|
||||
|
||||
// Load dashboard data
|
||||
async function loadDashboard() {
|
||||
if (isLoading) return;
|
||||
|
||||
isLoading = true;
|
||||
updateRefreshButton(true);
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
loadStats(),
|
||||
loadTopDevices()
|
||||
]);
|
||||
|
||||
// Update last refresh time
|
||||
updateLastRefreshTime();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load dashboard:', error);
|
||||
showToast('Erreur lors du chargement des données', 'error');
|
||||
} finally {
|
||||
isLoading = false;
|
||||
updateRefreshButton(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Update refresh button state
|
||||
function updateRefreshButton(loading) {
|
||||
const btn = document.getElementById('refreshBtn');
|
||||
if (!btn) return;
|
||||
|
||||
if (loading) {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '⏳ Chargement...';
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '🔄 Actualiser';
|
||||
}
|
||||
}
|
||||
|
||||
// Update last refresh time
|
||||
function updateLastRefreshTime() {
|
||||
const element = document.getElementById('lastUpdate');
|
||||
if (!element) return;
|
||||
|
||||
const now = new Date();
|
||||
element.textContent = `Mis à jour: ${now.toLocaleTimeString('fr-FR')}`;
|
||||
}
|
||||
|
||||
// Load statistics
|
||||
async function loadStats() {
|
||||
try {
|
||||
@@ -72,48 +112,74 @@ async function loadTopDevices() {
|
||||
|
||||
if (!data.items || data.items.length === 0) {
|
||||
showEmptyState(container, 'Aucun device trouvé. Exécutez un benchmark sur une machine pour commencer.', '📊');
|
||||
allDevices = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// Store all devices for filtering
|
||||
allDevices = data.items;
|
||||
|
||||
// Sort by global_score descending
|
||||
const sortedDevices = data.items.sort((a, b) => {
|
||||
const sortedDevices = allDevices.sort((a, b) => {
|
||||
const scoreA = a.last_benchmark?.global_score ?? -1;
|
||||
const scoreB = b.last_benchmark?.global_score ?? -1;
|
||||
return scoreB - scoreA;
|
||||
});
|
||||
|
||||
// Generate table HTML
|
||||
container.innerHTML = `
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Hostname</th>
|
||||
<th>Description</th>
|
||||
<th>Score Global</th>
|
||||
<th>CPU</th>
|
||||
<th>MEM</th>
|
||||
<th>DISK</th>
|
||||
<th>NET</th>
|
||||
<th>GPU</th>
|
||||
<th>Dernier Bench</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${sortedDevices.map((device, index) => createDeviceRow(device, index + 1)).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
// Render devices
|
||||
renderDevicesTable(sortedDevices);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load devices:', error);
|
||||
showError(container, 'Impossible de charger les devices. Vérifiez que le backend est accessible.');
|
||||
container.innerHTML = `
|
||||
<div class="error" style="text-align: center;">
|
||||
<p style="margin-bottom: 1rem;">❌ Impossible de charger les devices</p>
|
||||
<p style="font-size: 0.9rem; margin-bottom: 1rem;">${escapeHtml(error.message)}</p>
|
||||
<button class="btn btn-primary btn-sm" onclick="loadTopDevices()">🔄 Réessayer</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Render devices table
|
||||
function renderDevicesTable(devices) {
|
||||
const container = document.getElementById('devicesTable');
|
||||
|
||||
if (devices.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div style="text-align: center; padding: 2rem; color: var(--text-secondary);">
|
||||
<p>Aucun device trouvé avec ces critères de recherche.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Hostname</th>
|
||||
<th>Description</th>
|
||||
<th>Score Global</th>
|
||||
<th>CPU</th>
|
||||
<th>MEM</th>
|
||||
<th>DISK</th>
|
||||
<th>NET</th>
|
||||
<th>GPU</th>
|
||||
<th>Dernier Bench</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${devices.map((device, index) => createDeviceRow(device, index + 1)).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Create device row HTML
|
||||
function createDeviceRow(device, rank) {
|
||||
const bench = device.last_benchmark;
|
||||
@@ -167,13 +233,69 @@ async function copyBenchCommand() {
|
||||
}
|
||||
}
|
||||
|
||||
// Filter devices based on search query
|
||||
function filterDevices(query) {
|
||||
if (!query || query.trim() === '') {
|
||||
renderDevicesTable(allDevices);
|
||||
return;
|
||||
}
|
||||
|
||||
const lowerQuery = query.toLowerCase();
|
||||
const filtered = allDevices.filter(device => {
|
||||
const hostname = (device.hostname || '').toLowerCase();
|
||||
const description = (device.description || '').toLowerCase();
|
||||
const location = (device.location || '').toLowerCase();
|
||||
|
||||
return hostname.includes(lowerQuery) ||
|
||||
description.includes(lowerQuery) ||
|
||||
location.includes(lowerQuery);
|
||||
});
|
||||
|
||||
renderDevicesTable(filtered);
|
||||
}
|
||||
|
||||
// Debounced search
|
||||
const debouncedSearch = debounce((query) => {
|
||||
filterDevices(query);
|
||||
}, 300);
|
||||
|
||||
// Handle search input
|
||||
function handleSearch(event) {
|
||||
const query = event.target.value;
|
||||
debouncedSearch(query);
|
||||
}
|
||||
|
||||
// Clear search
|
||||
function clearSearch() {
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
if (searchInput) {
|
||||
searchInput.value = '';
|
||||
filterDevices('');
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh dashboard manually
|
||||
function refreshDashboard() {
|
||||
if (!isLoading) {
|
||||
loadDashboard();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize dashboard on page load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadDashboard();
|
||||
|
||||
// Setup search input listener
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', handleSearch);
|
||||
}
|
||||
|
||||
// Refresh every 30 seconds
|
||||
setInterval(loadDashboard, 30000);
|
||||
});
|
||||
|
||||
// Make copyBenchCommand available globally
|
||||
// Make functions available globally
|
||||
window.copyBenchCommand = copyBenchCommand;
|
||||
window.clearSearch = clearSearch;
|
||||
window.refreshDashboard = refreshDashboard;
|
||||
|
||||
@@ -34,9 +34,14 @@ async function loadDeviceDetail() {
|
||||
|
||||
// Render all sections
|
||||
renderDeviceHeader();
|
||||
renderHardwareSummary();
|
||||
renderLastBenchmark();
|
||||
renderMotherboardDetails();
|
||||
renderCPUDetails();
|
||||
renderMemoryDetails();
|
||||
renderStorageDetails();
|
||||
renderGPUDetails();
|
||||
renderNetworkDetails();
|
||||
renderOSDetails();
|
||||
renderBenchmarkResults();
|
||||
await loadBenchmarkHistory();
|
||||
await loadDocuments();
|
||||
await loadLinks();
|
||||
@@ -77,74 +82,371 @@ function renderDeviceHeader() {
|
||||
}
|
||||
}
|
||||
|
||||
// Render hardware summary
|
||||
function renderHardwareSummary() {
|
||||
// Render Motherboard Details
|
||||
function renderMotherboardDetails() {
|
||||
const snapshot = currentDevice.last_hardware_snapshot;
|
||||
const container = document.getElementById('motherboardDetails');
|
||||
|
||||
if (!snapshot) {
|
||||
document.getElementById('hardwareSummary').innerHTML =
|
||||
'<p style="color: var(--text-muted);">Aucune information hardware disponible</p>';
|
||||
container.innerHTML = '<p style="color: var(--text-muted);">Aucune information disponible</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// RAM usage info
|
||||
const ramTotalGB = Math.round((snapshot.ram_total_mb || 0) / 1024);
|
||||
const ramUsedMB = snapshot.ram_used_mb || 0;
|
||||
const ramFreeMB = snapshot.ram_free_mb || 0;
|
||||
const ramSharedMB = snapshot.ram_shared_mb || 0;
|
||||
// Helper to clean empty/whitespace-only strings
|
||||
const cleanValue = (val) => {
|
||||
if (!val || (typeof val === 'string' && val.trim() === '')) return 'N/A';
|
||||
return val;
|
||||
};
|
||||
|
||||
let ramValue = `${ramTotalGB} GB`;
|
||||
if (ramUsedMB > 0 || ramFreeMB > 0) {
|
||||
const usagePercent = ramTotalGB > 0 ? Math.round((ramUsedMB / (snapshot.ram_total_mb || 1)) * 100) : 0;
|
||||
ramValue = `${ramTotalGB} GB (${usagePercent}% utilisé)<br><small>Utilisée: ${Math.round(ramUsedMB / 1024)}GB • Libre: ${Math.round(ramFreeMB / 1024)}GB`;
|
||||
if (ramSharedMB > 0) {
|
||||
ramValue += ` • Partagée: ${Math.round(ramSharedMB / 1024)}GB`;
|
||||
}
|
||||
ramValue += `<br>${snapshot.ram_slots_used || '?'} / ${snapshot.ram_slots_total || '?'} slots</small>`;
|
||||
} else {
|
||||
ramValue += `<br><small>${snapshot.ram_slots_used || '?'} / ${snapshot.ram_slots_total || '?'} slots</small>`;
|
||||
}
|
||||
|
||||
const hardwareItems = [
|
||||
{ label: 'CPU', icon: '🔲', value: `${snapshot.cpu_model || 'N/A'}<br><small>${snapshot.cpu_cores || 0}C / ${snapshot.cpu_threads || 0}T @ ${snapshot.cpu_max_freq_ghz || snapshot.cpu_base_freq_ghz || '?'} GHz</small>` },
|
||||
{ label: 'RAM', icon: '💾', value: ramValue },
|
||||
{ label: 'GPU', icon: '🎮', value: snapshot.gpu_model || snapshot.gpu_summary || 'N/A' },
|
||||
{ label: 'Stockage', icon: '💿', value: snapshot.storage_summary || 'N/A' },
|
||||
{ label: 'Réseau', icon: '🌐', value: snapshot.network_interfaces_json ? `${JSON.parse(snapshot.network_interfaces_json).length} interface(s)` : 'N/A' },
|
||||
{ label: 'Carte mère', icon: '⚡', value: `${snapshot.motherboard_vendor || ''} ${snapshot.motherboard_model || 'N/A'}` },
|
||||
{ label: 'OS', icon: '🐧', value: `${snapshot.os_name || 'N/A'} ${snapshot.os_version || ''}<br><small>Kernel ${snapshot.kernel_version || 'N/A'}</small>` },
|
||||
{ label: 'Architecture', icon: '🏗️', value: snapshot.architecture || 'N/A' },
|
||||
{ label: 'Virtualisation', icon: '📦', value: snapshot.virtualization_type || 'none' }
|
||||
const items = [
|
||||
{ label: 'Fabricant', value: cleanValue(snapshot.motherboard_vendor) },
|
||||
{ label: 'Modèle', value: cleanValue(snapshot.motherboard_model) },
|
||||
{ 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` }
|
||||
];
|
||||
|
||||
document.getElementById('hardwareSummary').innerHTML = hardwareItems.map(item => `
|
||||
<div class="hardware-item">
|
||||
<div class="hardware-item-label">${item.icon} ${item.label}</div>
|
||||
<div class="hardware-item-value">${item.value}</div>
|
||||
container.innerHTML = `
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem;">
|
||||
${items.map(item => `
|
||||
<div class="hardware-item">
|
||||
<div class="hardware-item-label">${item.label}</div>
|
||||
<div class="hardware-item-value">${escapeHtml(String(item.value))}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`).join('');
|
||||
`;
|
||||
}
|
||||
|
||||
// Render last benchmark scores
|
||||
function renderLastBenchmark() {
|
||||
const bench = currentDevice.last_benchmark;
|
||||
// Render CPU Details
|
||||
function renderCPUDetails() {
|
||||
const snapshot = currentDevice.last_hardware_snapshot;
|
||||
const container = document.getElementById('cpuDetails');
|
||||
|
||||
if (!bench) {
|
||||
document.getElementById('lastBenchmark').innerHTML =
|
||||
'<p style="color: var(--text-muted);">Aucun benchmark disponible</p>';
|
||||
if (!snapshot) {
|
||||
container.innerHTML = '<p style="color: var(--text-muted);">Aucune information disponible</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('lastBenchmark').innerHTML = `
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<span style="color: var(--text-secondary);">Date: </span>
|
||||
const items = [
|
||||
{ label: 'Fabricant', value: snapshot.cpu_vendor || 'N/A' },
|
||||
{ label: 'Modèle', value: snapshot.cpu_model || 'N/A' },
|
||||
{ label: 'Microarchitecture', value: snapshot.cpu_microarchitecture || 'N/A' },
|
||||
{ label: 'Cores', value: snapshot.cpu_cores != null ? snapshot.cpu_cores : 'N/A' },
|
||||
{ label: 'Threads', value: snapshot.cpu_threads != null ? snapshot.cpu_threads : 'N/A' },
|
||||
{ label: 'Fréquence de base', value: snapshot.cpu_base_freq_ghz ? `${snapshot.cpu_base_freq_ghz} GHz` : 'N/A' },
|
||||
{ label: 'Fréquence max', value: snapshot.cpu_max_freq_ghz ? `${snapshot.cpu_max_freq_ghz} GHz` : 'N/A' },
|
||||
{ label: 'TDP', value: snapshot.cpu_tdp_w ? `${snapshot.cpu_tdp_w} W` : 'N/A' },
|
||||
{ label: 'Cache L1', value: snapshot.cpu_cache_l1_kb ? `${snapshot.cpu_cache_l1_kb} KB` : 'N/A' },
|
||||
{ label: 'Cache L2', value: snapshot.cpu_cache_l2_kb ? `${snapshot.cpu_cache_l2_kb} KB` : 'N/A' },
|
||||
{ label: 'Cache L3', value: snapshot.cpu_cache_l3_kb ? `${snapshot.cpu_cache_l3_kb} KB` : 'N/A' }
|
||||
];
|
||||
|
||||
let html = `
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;">
|
||||
${items.map(item => `
|
||||
<div class="hardware-item">
|
||||
<div class="hardware-item-label">${item.label}</div>
|
||||
<div class="hardware-item-value">${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: 1.5rem;">
|
||||
<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, 50).map(flag => `<span class="badge badge-muted">${escapeHtml(flag)}</span>`).join('')}
|
||||
${flags.length > 50 ? `<span class="badge">+${flags.length - 50} autres...</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse CPU flags:', e);
|
||||
}
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// Render Memory Details
|
||||
function renderMemoryDetails() {
|
||||
const snapshot = currentDevice.last_hardware_snapshot;
|
||||
const container = document.getElementById('memoryDetails');
|
||||
|
||||
if (!snapshot) {
|
||||
container.innerHTML = '<p style="color: var(--text-muted);">Aucune information disponible</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const ramTotalGB = Math.round((snapshot.ram_total_mb || 0) / 1024);
|
||||
const ramUsedGB = snapshot.ram_used_mb != null ? Math.round(snapshot.ram_used_mb / 1024) : null;
|
||||
const ramFreeGB = snapshot.ram_free_mb != null ? Math.round(snapshot.ram_free_mb / 1024) : null;
|
||||
const ramSharedGB = snapshot.ram_shared_mb != null ? Math.round(snapshot.ram_shared_mb / 1024) : null;
|
||||
const usagePercent = (ramTotalGB > 0 && snapshot.ram_used_mb != null) ? Math.round((snapshot.ram_used_mb / snapshot.ram_total_mb) * 100) : null;
|
||||
|
||||
const items = [
|
||||
{ label: 'Capacité totale', value: `${ramTotalGB} GB` },
|
||||
{ label: 'Mémoire utilisée', value: ramUsedGB != null ? `${ramUsedGB} GB${usagePercent != null ? ` (${usagePercent}%)` : ''}` : 'N/A' },
|
||||
{ label: 'Mémoire libre', value: ramFreeGB != null ? `${ramFreeGB} GB` : 'N/A' },
|
||||
{ label: 'Mémoire partagée', value: ramSharedGB != null ? `${ramSharedGB} GB` : 'N/A' },
|
||||
{ label: 'Slots utilisés', value: `${snapshot.ram_slots_used || '?'} / ${snapshot.ram_slots_total || '?'}` },
|
||||
{ label: 'ECC', value: snapshot.ram_ecc ? 'Oui' : 'Non' }
|
||||
];
|
||||
|
||||
let html = `
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 1.5rem;">
|
||||
${items.map(item => `
|
||||
<div class="hardware-item">
|
||||
<div class="hardware-item-label">${item.label}</div>
|
||||
<div class="hardware-item-value">${escapeHtml(String(item.value))}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// RAM Layout (DIMM details)
|
||||
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;">
|
||||
<div style="font-weight: 600; margin-bottom: 0.75rem; color: var(--text-secondary);">Configuration des barrettes :</div>
|
||||
<div style="display: grid; gap: 0.75rem;">
|
||||
${layout.map(dimm => `
|
||||
<div style="border: 1px solid var(--border-color); border-radius: 8px; padding: 1rem; display: grid; grid-template-columns: auto 1fr; gap: 0.5rem 1rem; align-items: center;">
|
||||
<strong style="color: var(--color-primary);">Slot ${escapeHtml(dimm.slot || 'N/A')}</strong>
|
||||
<div>
|
||||
${dimm.size_mb ? `${Math.round(dimm.size_mb / 1024)} GB` : 'N/A'}
|
||||
${dimm.type ? `• ${escapeHtml(dimm.type)}` : ''}
|
||||
${dimm.speed_mhz ? `• ${dimm.speed_mhz} MHz` : ''}
|
||||
</div>
|
||||
${dimm.vendor || dimm.manufacturer ? `
|
||||
<span style="color: var(--text-secondary);">Fabricant</span>
|
||||
<span>${escapeHtml(dimm.vendor || dimm.manufacturer)}</span>
|
||||
` : ''}
|
||||
${dimm.part_number ? `
|
||||
<span style="color: var(--text-secondary);">Part Number</span>
|
||||
<span><code style="font-size: 0.85rem;">${escapeHtml(dimm.part_number)}</code></span>
|
||||
` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse RAM layout:', e);
|
||||
}
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// Render Storage Details
|
||||
function renderStorageDetails() {
|
||||
const snapshot = currentDevice.last_hardware_snapshot;
|
||||
const container = document.getElementById('storageDetails');
|
||||
|
||||
if (!snapshot) {
|
||||
container.innerHTML = '<p style="color: var(--text-muted);">Aucune information disponible</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
|
||||
// Parse storage devices
|
||||
if (snapshot.storage_devices_json) {
|
||||
try {
|
||||
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: 1rem;">';
|
||||
|
||||
devices.forEach(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)';
|
||||
|
||||
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.1rem;">
|
||||
${typeIcon} ${escapeHtml(disk.name || disk.device || 'N/A')}
|
||||
</div>
|
||||
<div style="color: var(--text-secondary); font-size: 0.9rem; margin-top: 0.25rem;">
|
||||
${escapeHtml(disk.model || 'Unknown model')}
|
||||
</div>
|
||||
</div>
|
||||
${disk.smart_health ? `
|
||||
<span class="badge" style="background: ${healthColor}; color: white;">
|
||||
${escapeHtml(disk.smart_health)}
|
||||
</span>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 0.75rem; font-size: 0.9rem;">
|
||||
${disk.capacity_gb ? `
|
||||
<div>
|
||||
<strong>Capacité:</strong> ${disk.capacity_gb} GB
|
||||
</div>
|
||||
` : ''}
|
||||
${disk.type ? `
|
||||
<div>
|
||||
<strong>Type:</strong> ${escapeHtml(disk.type)}
|
||||
</div>
|
||||
` : ''}
|
||||
${disk.interface ? `
|
||||
<div>
|
||||
<strong>Interface:</strong> ${escapeHtml(disk.interface)}
|
||||
</div>
|
||||
` : ''}
|
||||
${disk.temperature_c ? `
|
||||
<div>
|
||||
<strong>Température:</strong> ${disk.temperature_c}°C
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
} else {
|
||||
html = '<p style="color: var(--text-muted);">Aucun disque détecté</p>';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to parse storage devices:', e);
|
||||
html = '<p style="color: var(--text-danger);">Erreur lors du parsing des données de stockage</p>';
|
||||
}
|
||||
} else {
|
||||
html = '<p style="color: var(--text-muted);">Aucune information de stockage disponible</p>';
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// Render GPU Details
|
||||
function renderGPUDetails() {
|
||||
const snapshot = currentDevice.last_hardware_snapshot;
|
||||
const container = document.getElementById('gpuDetails');
|
||||
|
||||
if (!snapshot) {
|
||||
container.innerHTML = '<p style="color: var(--text-muted);">Aucune information disponible</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!snapshot.gpu_vendor && !snapshot.gpu_model && !snapshot.gpu_summary) {
|
||||
container.innerHTML = '<p style="color: var(--text-muted);">Aucun GPU détecté</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const items = [
|
||||
{ label: 'Fabricant', value: snapshot.gpu_vendor || 'N/A' },
|
||||
{ label: 'Modèle', value: snapshot.gpu_model || snapshot.gpu_summary || 'N/A' },
|
||||
{ label: 'Driver', value: snapshot.gpu_driver_version || 'N/A' },
|
||||
{ label: 'Mémoire dédiée', value: snapshot.gpu_memory_dedicated_mb ? `${snapshot.gpu_memory_dedicated_mb} MB` : 'N/A' },
|
||||
{ label: 'Mémoire partagée', value: snapshot.gpu_memory_shared_mb ? `${snapshot.gpu_memory_shared_mb} MB` : 'N/A' }
|
||||
];
|
||||
|
||||
let html = `
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;">
|
||||
${items.map(item => `
|
||||
<div class="hardware-item">
|
||||
<div class="hardware-item-label">${item.label}</div>
|
||||
<div class="hardware-item-value">${escapeHtml(String(item.value))}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// API Support
|
||||
if (snapshot.gpu_api_support) {
|
||||
try {
|
||||
const apiSupport = typeof snapshot.gpu_api_support === 'string'
|
||||
? JSON.parse(snapshot.gpu_api_support)
|
||||
: snapshot.gpu_api_support;
|
||||
|
||||
if (Array.isArray(apiSupport) && apiSupport.length > 0) {
|
||||
html += `
|
||||
<div style="margin-top: 1rem;">
|
||||
<div style="font-weight: 600; margin-bottom: 0.5rem; color: var(--text-secondary);">APIs supportées :</div>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">
|
||||
${apiSupport.map(api => `<span class="badge badge-success">${escapeHtml(api)}</span>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse GPU API support:', e);
|
||||
}
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// Render OS Details
|
||||
function renderOSDetails() {
|
||||
const snapshot = currentDevice.last_hardware_snapshot;
|
||||
const container = document.getElementById('osDetails');
|
||||
|
||||
if (!snapshot) {
|
||||
container.innerHTML = '<p style="color: var(--text-muted);">Aucune information disponible</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const items = [
|
||||
{ label: 'Nom', 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: 'Virtualisation', value: snapshot.virtualization_type || 'none' }
|
||||
];
|
||||
|
||||
container.innerHTML = `
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;">
|
||||
${items.map(item => `
|
||||
<div class="hardware-item">
|
||||
<div class="hardware-item-label">${item.label}</div>
|
||||
<div class="hardware-item-value">${escapeHtml(String(item.value))}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Render Benchmark Results
|
||||
function renderBenchmarkResults() {
|
||||
const bench = currentDevice.last_benchmark;
|
||||
const container = document.getElementById('benchmarkResults');
|
||||
|
||||
if (!bench) {
|
||||
container.innerHTML = '<p style="color: var(--text-muted);">Aucun benchmark disponible</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = `
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<span style="color: var(--text-secondary);">Dernier benchmark: </span>
|
||||
<strong>${formatDate(bench.run_at)}</strong>
|
||||
<span style="margin-left: 1rem; color: var(--text-secondary);">Version: </span>
|
||||
<strong>${escapeHtml(bench.bench_script_version || 'N/A')}</strong>
|
||||
</div>
|
||||
|
||||
<div class="score-grid">
|
||||
${createScoreBadge(bench.global_score, 'Global')}
|
||||
${createScoreBadge(bench.global_score, 'Score Global')}
|
||||
${createScoreBadge(bench.cpu_score, 'CPU')}
|
||||
${createScoreBadge(bench.memory_score, 'Mémoire')}
|
||||
${createScoreBadge(bench.disk_score, 'Disque')}
|
||||
@@ -152,19 +454,18 @@ function renderLastBenchmark() {
|
||||
${createScoreBadge(bench.gpu_score, 'GPU')}
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 1rem;">
|
||||
<div style="margin-top: 1.5rem;">
|
||||
<button class="btn btn-secondary btn-sm" onclick="viewBenchmarkDetails(${bench.id})">
|
||||
Voir les détails complets (JSON)
|
||||
📋 Voir les détails complets (JSON)
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Render network details
|
||||
// Render Network Details
|
||||
function renderNetworkDetails() {
|
||||
const container = document.getElementById('networkDetails');
|
||||
const snapshot = currentDevice.last_hardware_snapshot;
|
||||
const bench = currentDevice.last_benchmark;
|
||||
|
||||
if (!snapshot || !snapshot.network_interfaces_json) {
|
||||
container.innerHTML = '<p style="color: var(--text-muted);">Aucune information réseau disponible</p>';
|
||||
@@ -172,7 +473,9 @@ function renderNetworkDetails() {
|
||||
}
|
||||
|
||||
try {
|
||||
const interfaces = JSON.parse(snapshot.network_interfaces_json);
|
||||
const interfaces = typeof snapshot.network_interfaces_json === 'string'
|
||||
? JSON.parse(snapshot.network_interfaces_json)
|
||||
: snapshot.network_interfaces_json;
|
||||
|
||||
if (!interfaces || interfaces.length === 0) {
|
||||
container.innerHTML = '<p style="color: var(--text-muted);">Aucune interface réseau détectée</p>';
|
||||
@@ -190,87 +493,45 @@ function renderNetworkDetails() {
|
||||
: (wol === false ? '<span class="badge badge-muted" style="margin-left: 0.5rem;">WoL ✗</span>' : '');
|
||||
|
||||
html += `
|
||||
<div class="hardware-item" style="border: 1px solid var(--border-color); border-radius: 8px; padding: 1rem;">
|
||||
<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);">${typeIcon} ${escapeHtml(iface.name)}</div>
|
||||
<div style="font-weight: 600; color: var(--color-primary); font-size: 1.05rem;">${typeIcon} ${escapeHtml(iface.name)}</div>
|
||||
<div style="color: var(--text-secondary); font-size: 0.9rem; margin-top: 0.25rem;">${escapeHtml(iface.type || 'unknown')}</div>
|
||||
</div>
|
||||
<div>${wolBadge}</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 0.5rem; font-size: 0.9rem;">
|
||||
${iface.ip ? `<div><strong>IP:</strong> ${escapeHtml(iface.ip)}</div>` : ''}
|
||||
${iface.mac ? `<div><strong>MAC:</strong> <code>${escapeHtml(iface.mac)}</code></div>` : ''}
|
||||
${iface.speed_mbps ? `<div><strong>Vitesse:</strong> ${iface.speed_mbps} Mbps</div>` : ''}
|
||||
${iface.driver ? `<div><strong>Driver:</strong> ${escapeHtml(iface.driver)}</div>` : ''}
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 0.75rem; font-size: 0.9rem;">
|
||||
${iface.ip ? `
|
||||
<div>
|
||||
<strong style="color: var(--text-secondary);">Adresse IP:</strong><br>
|
||||
<code>${escapeHtml(iface.ip)}</code>
|
||||
</div>
|
||||
` : ''}
|
||||
${iface.mac ? `
|
||||
<div>
|
||||
<strong style="color: var(--text-secondary);">MAC:</strong><br>
|
||||
<code>${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>
|
||||
${escapeHtml(iface.driver)}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
// Network benchmark results (iperf3)
|
||||
if (bench && bench.network_score !== null && bench.network_score !== undefined) {
|
||||
let netBenchHtml = '<div style="border: 2px solid var(--color-info); border-radius: 8px; padding: 1rem; margin-top: 1rem;">';
|
||||
netBenchHtml += '<div style="font-weight: 600; color: var(--color-info); margin-bottom: 0.75rem;">📈 Résultats Benchmark Réseau (iperf3)</div>';
|
||||
netBenchHtml += '<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem;">';
|
||||
|
||||
// Try to parse network_results_json if available
|
||||
let uploadMbps = null;
|
||||
let downloadMbps = null;
|
||||
let pingMs = null;
|
||||
|
||||
if (bench.network_results_json) {
|
||||
try {
|
||||
const netResults = typeof bench.network_results_json === 'string'
|
||||
? JSON.parse(bench.network_results_json)
|
||||
: bench.network_results_json;
|
||||
uploadMbps = netResults.upload_mbps;
|
||||
downloadMbps = netResults.download_mbps;
|
||||
pingMs = netResults.ping_ms;
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse network_results_json:', e);
|
||||
}
|
||||
}
|
||||
|
||||
if (uploadMbps !== null && uploadMbps !== undefined) {
|
||||
netBenchHtml += `
|
||||
<div style="text-align: center;">
|
||||
<div style="font-size: 1.5rem; font-weight: 600; color: var(--color-success);">↑ ${uploadMbps.toFixed(2)}</div>
|
||||
<div style="color: var(--text-secondary); font-size: 0.85rem;">Upload Mbps</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (downloadMbps !== null && downloadMbps !== undefined) {
|
||||
netBenchHtml += `
|
||||
<div style="text-align: center;">
|
||||
<div style="font-size: 1.5rem; font-weight: 600; color: var(--color-info);">↓ ${downloadMbps.toFixed(2)}</div>
|
||||
<div style="color: var(--text-secondary); font-size: 0.85rem;">Download Mbps</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (pingMs !== null && pingMs !== undefined) {
|
||||
netBenchHtml += `
|
||||
<div style="text-align: center;">
|
||||
<div style="font-size: 1.5rem; font-weight: 600; color: var(--color-warning);">${typeof pingMs === 'number' ? pingMs.toFixed(2) : pingMs}</div>
|
||||
<div style="color: var(--text-secondary); font-size: 0.85rem;">Ping ms</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
netBenchHtml += `
|
||||
<div style="text-align: center;">
|
||||
<div style="font-size: 1.5rem; font-weight: 600; color: var(--color-primary);">${bench.network_score.toFixed(2)}</div>
|
||||
<div style="color: var(--text-secondary); font-size: 0.85rem;">Score</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
netBenchHtml += '</div></div>';
|
||||
html += netBenchHtml;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
container.innerHTML = html;
|
||||
|
||||
@@ -360,16 +621,58 @@ async function loadDocuments() {
|
||||
const container = document.getElementById('documentsList');
|
||||
|
||||
try {
|
||||
const docs = await api.getDeviceDocs(currentDeviceId);
|
||||
// Use documents from currentDevice (already loaded)
|
||||
const docs = currentDevice.documents || [];
|
||||
|
||||
if (!docs || docs.length === 0) {
|
||||
showEmptyState(container, 'Aucun document uploadé', '📄');
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = `
|
||||
<ul class="document-list">
|
||||
${docs.map(doc => `
|
||||
// Separate images from other documents
|
||||
const images = docs.filter(doc => doc.doc_type === 'image');
|
||||
const otherDocs = docs.filter(doc => doc.doc_type !== 'image');
|
||||
|
||||
let html = '';
|
||||
|
||||
// Display images with preview
|
||||
if (images.length > 0) {
|
||||
html += '<h4 style="margin-bottom: 1rem; color: var(--color-primary);">🖼️ Images</h4>';
|
||||
html += '<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 1rem; margin-bottom: 2rem;">';
|
||||
|
||||
images.forEach(doc => {
|
||||
const downloadUrl = api.getDocumentDownloadUrl(doc.id);
|
||||
html += `
|
||||
<div style="border: 1px solid var(--border-color); border-radius: 8px; overflow: hidden; background: var(--bg-secondary);">
|
||||
<div style="width: 100%; height: 200px; background: var(--bg-primary); display: flex; align-items: center; justify-content: center; overflow: hidden;">
|
||||
<img src="${downloadUrl}" alt="${escapeHtml(doc.filename)}" style="max-width: 100%; max-height: 100%; object-fit: contain; cursor: pointer;" onclick="window.open('${downloadUrl}', '_blank')">
|
||||
</div>
|
||||
<div style="padding: 0.75rem;">
|
||||
<div style="font-size: 0.9rem; font-weight: 500; color: var(--text-primary); margin-bottom: 0.5rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${escapeHtml(doc.filename)}">
|
||||
📎 ${escapeHtml(doc.filename)}
|
||||
</div>
|
||||
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 0.75rem;">
|
||||
${formatFileSize(doc.size_bytes)} • ${formatDate(doc.uploaded_at)}
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<a href="${downloadUrl}" class="btn btn-sm btn-secondary" download style="flex: 1; text-align: center;">⬇️ Télécharger</a>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteDocument(${doc.id})" style="flex: 1;">🗑️ Supprimer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// Display other documents (PDFs, manuals, etc.)
|
||||
if (otherDocs.length > 0) {
|
||||
html += '<h4 style="margin-bottom: 1rem; color: var(--color-primary);">📄 Autres Documents</h4>';
|
||||
html += '<ul class="document-list">';
|
||||
|
||||
otherDocs.forEach(doc => {
|
||||
html += `
|
||||
<li class="document-item">
|
||||
<div class="document-info">
|
||||
<span class="document-icon">${getDocIcon(doc.doc_type)}</span>
|
||||
@@ -381,13 +684,17 @@ async function loadDocuments() {
|
||||
</div>
|
||||
</div>
|
||||
<div class="document-actions">
|
||||
<a href="${api.getDocumentDownloadUrl(doc.id)}" class="btn btn-sm btn-secondary" download>Télécharger</a>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteDocument(${doc.id})">Supprimer</button>
|
||||
<a href="${api.getDocumentDownloadUrl(doc.id)}" class="btn btn-sm btn-secondary" download>⬇️ Télécharger</a>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteDocument(${doc.id})">🗑️ Supprimer</button>
|
||||
</div>
|
||||
</li>
|
||||
`).join('')}
|
||||
</ul>
|
||||
`;
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</ul>';
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load documents:', error);
|
||||
@@ -398,6 +705,7 @@ async function loadDocuments() {
|
||||
// Get document icon
|
||||
function getDocIcon(docType) {
|
||||
const icons = {
|
||||
image: '🖼️',
|
||||
manual: '📘',
|
||||
warranty: '📜',
|
||||
invoice: '🧾',
|
||||
@@ -428,7 +736,8 @@ async function uploadDocument() {
|
||||
fileInput.value = '';
|
||||
docTypeSelect.value = 'manual';
|
||||
|
||||
// Reload documents
|
||||
// Reload device to get updated documents
|
||||
currentDevice = await api.getDevice(currentDeviceId);
|
||||
await loadDocuments();
|
||||
|
||||
} catch (error) {
|
||||
@@ -446,6 +755,9 @@ async function deleteDocument(docId) {
|
||||
try {
|
||||
await api.deleteDocument(docId);
|
||||
showToast('Document supprimé', 'success');
|
||||
|
||||
// Reload device to get updated documents
|
||||
currentDevice = await api.getDevice(currentDeviceId);
|
||||
await loadDocuments();
|
||||
|
||||
} catch (error) {
|
||||
|
||||
@@ -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