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

979 lines
38 KiB
JavaScript
Executable File

// Linux BenchTools - Device Detail Logic
const { formatDate, formatRelativeTime, formatFileSize, createScoreBadge, getScoreBadgeText, escapeHtml, showError, showEmptyState, formatTags, initTabs, openModal, showToast, formatHardwareInfo, formatDuration, formatStorage } = window.BenchUtils;
const api = window.BenchAPI;
let currentDeviceId = null;
let currentDevice = null;
// Initialize page
document.addEventListener('DOMContentLoaded', async () => {
// Get device ID from URL
currentDeviceId = window.BenchUtils.getUrlParameter('id');
if (!currentDeviceId) {
document.getElementById('loadingState').innerHTML = '<div class="error">Device ID manquant dans l\'URL</div>';
return;
}
// Initialize tabs
initTabs('.tabs-container');
// Load device data
await loadDeviceDetail();
});
// Load device detail
async function loadDeviceDetail() {
try {
currentDevice = await api.getDevice(currentDeviceId);
// Show content, hide loading
document.getElementById('loadingState').style.display = 'none';
document.getElementById('deviceContent').style.display = 'block';
// Render all sections
renderDeviceHeader();
renderMotherboardDetails();
renderCPUDetails();
renderMemoryDetails();
renderStorageDetails();
renderGPUDetails();
renderNetworkDetails();
renderOSDetails();
renderBenchmarkResults();
await loadBenchmarkHistory();
await loadDocuments();
await loadLinks();
const deleteBtn = document.getElementById('deleteDeviceBtn');
if (deleteBtn) {
deleteBtn.addEventListener('click', handleDeleteDevice);
}
} catch (error) {
console.error('Failed to load device:', error);
document.getElementById('loadingState').innerHTML =
`<div class="error">Erreur lors du chargement du device: ${escapeHtml(error.message)}</div>`;
}
}
// Render device header
function renderDeviceHeader() {
document.getElementById('deviceHostname').textContent = currentDevice.hostname;
document.getElementById('deviceDescription').textContent = currentDevice.description || 'Aucune description';
// Global score
const globalScore = currentDevice.last_benchmark?.global_score;
document.getElementById('globalScoreContainer').innerHTML =
globalScore !== null && globalScore !== undefined
? `<div class="${window.BenchUtils.getScoreBadgeClass(globalScore)}" style="font-size: 2rem; min-width: 80px; height: 80px; display: flex; align-items: center; justify-content: center;">${getScoreBadgeText(globalScore)}</div>`
: '<span class="badge">N/A</span>';
// Meta information
const metaParts = [];
if (currentDevice.location) metaParts.push(`📍 ${escapeHtml(currentDevice.location)}`);
if (currentDevice.owner) metaParts.push(`👤 ${escapeHtml(currentDevice.owner)}`);
if (currentDevice.asset_tag) metaParts.push(`🏷️ ${escapeHtml(currentDevice.asset_tag)}`);
if (currentDevice.last_benchmark?.run_at) metaParts.push(`⏱️ ${formatRelativeTime(currentDevice.last_benchmark.run_at)}`);
document.getElementById('deviceMeta').innerHTML = metaParts.map(part =>
`<span style="color: var(--text-secondary);">${part}</span>`
).join('');
// Tags
if (currentDevice.tags) {
document.getElementById('deviceTags').innerHTML = formatTags(currentDevice.tags);
}
}
// Render Motherboard Details
function renderMotherboardDetails() {
const snapshot = currentDevice.last_hardware_snapshot;
const container = document.getElementById('motherboardDetails');
if (!container) return;
container.innerHTML = HardwareRenderer.renderMotherboardDetails(snapshot);
}
// Render CPU Details
function renderCPUDetails() {
const snapshot = currentDevice.last_hardware_snapshot;
const container = document.getElementById('cpuDetails');
if (!snapshot) {
container.innerHTML = '<p style="color: var(--text-muted);">Aucune information disponible</p>';
return;
}
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 diskType = (disk.type || '').toString().toLowerCase();
const diskInterface = (disk.interface || '').toString().toLowerCase();
const isSsd = diskType === 'ssd' || diskType === 'nvme' || diskInterface === 'nvme';
const typeIcon = isSsd ? '💾' : '💿';
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> ${formatStorage(disk.capacity_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 !== null && disk.temperature_c !== undefined) ? `
<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>';
}
}
// Parse partitions
if (snapshot.partitions_json) {
try {
const partitions = typeof snapshot.partitions_json === 'string'
? JSON.parse(snapshot.partitions_json)
: snapshot.partitions_json;
if (Array.isArray(partitions) && partitions.length > 0) {
html += `
<div style="margin-top: 1.5rem;">
<h4 style="margin-bottom: 0.75rem; color: var(--text-secondary);">Partitions et volumes</h4>
<div style="overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="text-align: left; border-bottom: 1px solid var(--border-color); color: var(--text-secondary);">
<th style="padding: 0.5rem;">Partition</th>
<th style="padding: 0.5rem;">Montage</th>
<th style="padding: 0.5rem;">Type</th>
<th style="padding: 0.5rem;">Utilisé</th>
<th style="padding: 0.5rem;">Libre</th>
<th style="padding: 0.5rem;">Total</th>
</tr>
</thead>
<tbody>
${partitions.map(part => {
const used = typeof part.used_gb === 'number' ? formatStorage(part.used_gb) : 'N/A';
const free = typeof part.free_gb === 'number'
? formatStorage(part.free_gb)
: (typeof part.total_gb === 'number' && typeof part.used_gb === 'number'
? formatStorage(part.total_gb - part.used_gb)
: 'N/A');
const total = typeof part.total_gb === 'number' ? formatStorage(part.total_gb) : 'N/A';
return `
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 0.5rem; font-weight: 600;">${escapeHtml(part.name || 'N/A')}</td>
<td style="padding: 0.5rem;">${part.mount_point ? escapeHtml(part.mount_point) : '<span style="color: var(--text-muted);">Non monté</span>'}</td>
<td style="padding: 0.5rem;">${part.fs_type ? escapeHtml(part.fs_type) : 'N/A'}</td>
<td style="padding: 0.5rem;">${used}</td>
<td style="padding: 0.5rem;">${free}</td>
<td style="padding: 0.5rem;">${total}</td>
</tr>
`;
}).join('')}
</tbody>
</table>
</div>
</div>
`;
}
} catch (error) {
console.error('Failed to parse partitions:', error);
html += '<p style="color: var(--color-danger); margin-top: 1rem;">Erreur lors de la lecture des partitions</p>';
}
}
if (snapshot.network_shares_json) {
try {
const shares = typeof snapshot.network_shares_json === 'string'
? JSON.parse(snapshot.network_shares_json)
: snapshot.network_shares_json;
if (Array.isArray(shares) && shares.length > 0) {
html += `
<div style="margin-top: 1.5rem;">
<h4 style="margin-bottom: 0.75rem; color: var(--text-secondary);">Partages réseau montés</h4>
<div style="overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="text-align: left; border-bottom: 1px solid var(--border-color); color: var(--text-secondary);">
<th style="padding: 0.5rem;">Source</th>
<th style="padding: 0.5rem;">Montage</th>
<th style="padding: 0.5rem;">Protocole</th>
<th style="padding: 0.5rem;">Type</th>
<th style="padding: 0.5rem;">Utilisé</th>
<th style="padding: 0.5rem;">Libre</th>
<th style="padding: 0.5rem;">Total</th>
<th style="padding: 0.5rem;">Options</th>
</tr>
</thead>
<tbody>
${shares.map(share => `
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 0.5rem;">${escapeHtml(share.source || 'N/A')}</td>
<td style="padding: 0.5rem;">${escapeHtml(share.mount_point || 'N/A')}</td>
<td style="padding: 0.5rem;">${escapeHtml(share.protocol || share.fs_type || 'N/A')}</td>
<td style="padding: 0.5rem;">${share.fs_type ? escapeHtml(share.fs_type) : 'N/A'}</td>
<td style="padding: 0.5rem;">${typeof share.used_gb === 'number' ? formatStorage(share.used_gb) : 'N/A'}</td>
<td style="padding: 0.5rem;">${typeof share.free_gb === 'number' ? formatStorage(share.free_gb) : 'N/A'}</td>
<td style="padding: 0.5rem;">${typeof share.total_gb === 'number' ? formatStorage(share.total_gb) : 'N/A'}</td>
<td style="padding: 0.5rem; max-width: 200px;">${share.options ? escapeHtml(share.options) : '<span style="color: var(--text-muted);">N/A</span>'}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
`;
}
} catch (error) {
console.error('Failed to parse network shares:', error);
html += '<p style="color: var(--color-danger); margin-top: 1rem;">Erreur lors de la lecture des partages réseau</p>';
}
}
if (!html) {
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 displayServer = snapshot.display_server || snapshot.session_type || 'N/A';
const resolution = snapshot.screen_resolution || 'N/A';
const lastBoot = snapshot.last_boot_time || 'N/A';
const uptime = snapshot.uptime_seconds != null ? formatDuration(snapshot.uptime_seconds) : 'N/A';
const battery = snapshot.battery_percentage != null
? `${snapshot.battery_percentage}%${snapshot.battery_status ? ` (${snapshot.battery_status})` : ''}`
: 'N/A';
const items = [
{ label: '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: 'Environnement', value: snapshot.desktop_environment || 'N/A' },
{ label: 'Session', value: displayServer },
{ label: 'Résolution écran', value: resolution },
{ label: 'Dernier boot', value: lastBoot },
{ label: 'Uptime', value: uptime },
{ label: 'Batterie', value: battery },
{ label: 'Virtualisation', value: snapshot.virtualization_type || 'none' }
];
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>
`;
}
async function handleDeleteDevice() {
if (!currentDevice) return;
const confirmed = confirm(`Voulez-vous vraiment supprimer le device "${currentDevice.hostname}" ? Cette action est définitive.`);
if (!confirmed) {
return;
}
try {
await api.deleteDevice(currentDevice.id);
showToast('Device supprimé avec succès', 'success');
setTimeout(() => {
window.location.href = 'devices.html';
}, 800);
} catch (error) {
console.error('Failed to delete device:', error);
showToast(error.message || 'Impossible de supprimer le device', 'error');
}
}
// 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, 'Score Global')}
${createScoreBadge(bench.cpu_score, 'CPU')}
${createScoreBadge(bench.memory_score, 'Mémoire')}
${createScoreBadge(bench.disk_score, 'Disque')}
${createScoreBadge(bench.network_score, 'Réseau')}
${createScoreBadge(bench.gpu_score, 'GPU')}
</div>
<div style="margin-top: 1.5rem;">
<button class="btn btn-secondary btn-sm" onclick="viewBenchmarkDetails(${bench.id})">
📋 Voir les détails complets (JSON)
</button>
</div>
`;
}
// Render Network Details
function renderNetworkDetails() {
const container = document.getElementById('networkDetails');
const snapshot = currentDevice.last_hardware_snapshot;
if (!snapshot || !snapshot.network_interfaces_json) {
container.innerHTML = '<p style="color: var(--text-muted);">Aucune information réseau disponible</p>';
return;
}
try {
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>';
return;
}
let html = '<div style="display: grid; gap: 1rem;">';
// Interface details
interfaces.forEach(iface => {
const typeIcon = iface.type === 'ethernet' ? '🔌' : (iface.type === 'wifi' ? '📡' : '🌐');
const wol = iface.wake_on_lan;
const wolBadge = wol === true
? '<span class="badge badge-success" style="margin-left: 0.5rem;">WoL ✓</span>'
: (wol === false ? '<span class="badge badge-muted" style="margin-left: 0.5rem;">WoL ✗</span>' : '');
html += `
<div style="border: 1px solid var(--border-color); border-radius: 8px; padding: 1rem;">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 0.75rem;">
<div>
<div style="font-weight: 600; color: var(--color-primary); font-size: 1.05rem;">${typeIcon} ${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; 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>
`;
});
html += '</div>';
container.innerHTML = html;
} catch (error) {
console.error('Failed to parse network interfaces:', error);
container.innerHTML = '<p style="color: var(--text-danger);">Erreur lors du parsing des données réseau</p>';
}
}
// Load benchmark history
async function loadBenchmarkHistory() {
const container = document.getElementById('benchmarkHistory');
try {
const data = await api.getDeviceBenchmarks(currentDeviceId, { limit: 20 });
if (!data.items || data.items.length === 0) {
showEmptyState(container, 'Aucun benchmark dans l\'historique', '📊');
return;
}
container.innerHTML = `
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Date</th>
<th>Score Global</th>
<th>CPU</th>
<th>MEM</th>
<th>DISK</th>
<th>NET</th>
<th>GPU</th>
<th>Version</th>
<th>Action</th>
</tr>
</thead>
<tbody>
${data.items.map(bench => `
<tr>
<td>${formatDate(bench.run_at)}</td>
<td><span class="${window.BenchUtils.getScoreBadgeClass(bench.global_score)}">${getScoreBadgeText(bench.global_score)}</span></td>
<td><span class="${window.BenchUtils.getScoreBadgeClass(bench.cpu_score)}">${getScoreBadgeText(bench.cpu_score)}</span></td>
<td><span class="${window.BenchUtils.getScoreBadgeClass(bench.memory_score)}">${getScoreBadgeText(bench.memory_score)}</span></td>
<td><span class="${window.BenchUtils.getScoreBadgeClass(bench.disk_score)}">${getScoreBadgeText(bench.disk_score)}</span></td>
<td><span class="${window.BenchUtils.getScoreBadgeClass(bench.network_score)}">${getScoreBadgeText(bench.network_score)}</span></td>
<td><span class="${window.BenchUtils.getScoreBadgeClass(bench.gpu_score)}">${getScoreBadgeText(bench.gpu_score)}</span></td>
<td><small>${escapeHtml(bench.bench_script_version || 'N/A')}</small></td>
<td>
<button class="btn btn-sm btn-secondary" onclick="viewBenchmarkDetails(${bench.id})">Détails</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
} catch (error) {
console.error('Failed to load benchmarks:', error);
showError(container, 'Erreur lors du chargement de l\'historique');
}
}
// View benchmark details
async function viewBenchmarkDetails(benchmarkId) {
const modalBody = document.getElementById('benchmarkModalBody');
openModal('benchmarkModal');
try {
const benchmark = await api.getBenchmark(benchmarkId);
modalBody.innerHTML = `
<div class="code-block" style="max-height: 500px; overflow-y: auto;">
<pre><code>${JSON.stringify(benchmark.details || benchmark, null, 2)}</code></pre>
</div>
`;
} catch (error) {
console.error('Failed to load benchmark details:', error);
modalBody.innerHTML = `<div class="error">Erreur: ${escapeHtml(error.message)}</div>`;
}
}
// Load documents
async function loadDocuments() {
const container = document.getElementById('documentsList');
try {
// Use documents from currentDevice (already loaded)
const docs = currentDevice.documents || [];
if (!docs || docs.length === 0) {
showEmptyState(container, 'Aucun document uploadé', '📄');
return;
}
// 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>
<div>
<div class="document-name">${escapeHtml(doc.filename)}</div>
<div class="document-meta">
${doc.doc_type}${formatFileSize(doc.size_bytes)}${formatDate(doc.uploaded_at)}
</div>
</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>
</div>
</li>
`;
});
html += '</ul>';
}
container.innerHTML = html;
} catch (error) {
console.error('Failed to load documents:', error);
showError(container, 'Erreur lors du chargement des documents');
}
}
// Get document icon
function getDocIcon(docType) {
const icons = {
image: '🖼️',
manual: '📘',
warranty: '📜',
invoice: '🧾',
photo: '📷',
other: '📄'
};
return icons[docType] || '📄';
}
// Upload document
async function uploadDocument() {
const fileInput = document.getElementById('fileInput');
const docTypeSelect = document.getElementById('docTypeSelect');
if (!fileInput.files || fileInput.files.length === 0) {
showToast('Veuillez sélectionner un fichier', 'error');
return;
}
const file = fileInput.files[0];
const docType = docTypeSelect.value;
try {
await api.uploadDocument(currentDeviceId, file, docType);
showToast('Document uploadé avec succès', 'success');
// Reset form
fileInput.value = '';
docTypeSelect.value = 'manual';
// Reload device to get updated documents
currentDevice = await api.getDevice(currentDeviceId);
await loadDocuments();
} catch (error) {
console.error('Failed to upload document:', error);
showToast('Erreur lors de l\'upload: ' + error.message, 'error');
}
}
// Delete document
async function deleteDocument(docId) {
if (!confirm('Êtes-vous sûr de vouloir supprimer ce document ?')) {
return;
}
try {
await api.deleteDocument(docId);
showToast('Document supprimé', 'success');
// Reload device to get updated documents
currentDevice = await api.getDevice(currentDeviceId);
await loadDocuments();
} catch (error) {
console.error('Failed to delete document:', error);
showToast('Erreur lors de la suppression: ' + error.message, 'error');
}
}
// Load links
async function loadLinks() {
const container = document.getElementById('linksList');
try {
const links = await api.getDeviceLinks(currentDeviceId);
if (!links || links.length === 0) {
showEmptyState(container, 'Aucun lien ajouté', '🔗');
return;
}
container.innerHTML = `
<ul class="link-list">
${links.map(link => `
<li class="link-item">
<div class="link-info">
<a href="${escapeHtml(link.url)}" target="_blank" rel="noopener noreferrer">
🔗 ${escapeHtml(link.label)}
</a>
<div class="link-label">${escapeHtml(link.url)}</div>
</div>
<div class="link-actions">
<button class="btn btn-sm btn-danger" onclick="deleteLink(${link.id})">Supprimer</button>
</div>
</li>
`).join('')}
</ul>
`;
} catch (error) {
console.error('Failed to load links:', error);
showError(container, 'Erreur lors du chargement des liens');
}
}
// Add link
async function addLink() {
const labelInput = document.getElementById('linkLabel');
const urlInput = document.getElementById('linkUrl');
const label = labelInput.value.trim();
const url = urlInput.value.trim();
if (!label || !url) {
showToast('Veuillez remplir tous les champs', 'error');
return;
}
try {
await api.addDeviceLink(currentDeviceId, { label, url });
showToast('Lien ajouté avec succès', 'success');
// Reset form
labelInput.value = '';
urlInput.value = '';
// Reload links
await loadLinks();
} catch (error) {
console.error('Failed to add link:', error);
showToast('Erreur lors de l\'ajout: ' + error.message, 'error');
}
}
// Delete link
async function deleteLink(linkId) {
if (!confirm('Êtes-vous sûr de vouloir supprimer ce lien ?')) {
return;
}
try {
await api.deleteLink(linkId);
showToast('Lien supprimé', 'success');
await loadLinks();
} catch (error) {
console.error('Failed to delete link:', error);
showToast('Erreur lors de la suppression: ' + error.message, 'error');
}
}
// Make functions available globally
window.viewBenchmarkDetails = viewBenchmarkDetails;
window.uploadDocument = uploadDocument;
window.deleteDocument = deleteDocument;
window.addLink = addLink;
window.deleteLink = deleteLink;