add go bench client

This commit is contained in:
Gilles Soulier
2026-01-11 23:41:30 +01:00
parent c67befc549
commit 6abc70cdfe
80 changed files with 13311 additions and 61 deletions

View File

@@ -91,36 +91,8 @@ function renderDeviceHeader() {
function renderMotherboardDetails() {
const snapshot = currentDevice.last_hardware_snapshot;
const container = document.getElementById('motherboardDetails');
if (!snapshot) {
container.innerHTML = '<p style="color: var(--text-muted);">Aucune information disponible</p>';
return;
}
// Helper to clean empty/whitespace-only strings
const cleanValue = (val) => {
if (!val || (typeof val === 'string' && val.trim() === '')) return 'N/A';
return val;
};
const items = [
{ label: 'Fabricant', value: cleanValue(snapshot.motherboard_vendor) },
{ label: 'Modèle', value: cleanValue(snapshot.motherboard_model) },
{ label: '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` }
];
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>
`;
if (!container) return;
container.innerHTML = HardwareRenderer.renderMotherboardDetails(snapshot);
}
// Render CPU Details
@@ -279,7 +251,10 @@ function renderStorageDetails() {
html += '<div style="display: grid; gap: 1rem;">';
devices.forEach(disk => {
const typeIcon = disk.type === 'SSD' ? '💾' : '💿';
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)';
@@ -318,7 +293,7 @@ function renderStorageDetails() {
<strong>Interface:</strong> ${escapeHtml(disk.interface)}
</div>
` : ''}
${disk.temperature_c ? `
${(disk.temperature_c !== null && disk.temperature_c !== undefined) ? `
<div>
<strong>Température:</strong> ${disk.temperature_c}°C
</div>

View File

@@ -14,33 +14,34 @@ let editingNotes = false;
let editingUpgradeNotes = false;
let editingPurchase = false;
const SECTION_ICON_PATHS = {
motherboard: 'icons/icons8-motherboard-94.png',
cpu: 'icons/icons8-processor-94.png',
ram: 'icons/icons8-memory-slot-94.png',
storage: 'icons/icons8-ssd-94.png',
gpu: 'icons/icons8-gpu-64.png',
network: 'icons/icons8-network-cable-94.png',
usb: 'icons/icons8-usb-memory-stick-94.png',
pci: 'icons/icons8-pcie-48.png',
os: 'icons/icons8-operating-system-64.png',
shares: 'icons/icons8-shared-folder-94.png',
benchmarks: 'icons/icons8-benchmark-64.png',
metadata: 'icons/icons8-hardware-64.png',
images: 'icons/icons8-picture-48.png',
pdf: 'icons/icons8-bios-94.png',
links: 'icons/icons8-server-94.png',
tags: 'icons/icons8-check-mark-48.png',
notes: 'icons/icons8-edit-pencil-48.png',
purchase: 'icons/icons8-laptop-50.png',
upgrade: 'icons/icons8-workstation-94.png'
// Section icon mapping - uses data-icon with IconManager
const SECTION_ICON_NAMES = {
motherboard: 'motherboard',
cpu: 'cpu',
ram: 'memory',
storage: 'hdd',
gpu: 'gpu',
network: 'network',
usb: 'usb',
pci: 'pci',
os: 'desktop',
shares: 'folder',
benchmarks: 'chart-line',
metadata: 'info-circle',
images: 'image',
pdf: 'file-pdf',
links: 'link',
tags: 'tag',
notes: 'edit',
purchase: 'shopping-cart',
upgrade: 'rocket'
};
function getSectionIcon(key, altText) {
const src = SECTION_ICON_PATHS[key];
if (!src) return '';
const iconName = SECTION_ICON_NAMES[key];
if (!iconName) return '';
const safeAlt = utils.escapeHtml(altText || key);
return `<img src="${src}" alt="${safeAlt}" class="section-icon" loading="lazy">`;
return `<span class="section-icon" data-icon="${iconName}" title="${safeAlt}"></span>`;
}
// Load devices
@@ -1083,6 +1084,117 @@ async function viewBenchmarkDetails(benchmarkId) {
}
}
// Render IP Display with edit capability
function renderIPDisplay(snapshot, device) {
// Extract non-loopback IPs
const networkInterfaces = snapshot?.network_interfaces_json ?
(typeof snapshot.network_interfaces_json === 'string' ? JSON.parse(snapshot.network_interfaces_json) : snapshot.network_interfaces_json) :
[];
const ips = networkInterfaces
.filter(iface => iface.ipv4 && iface.ipv4 !== '127.0.0.1' && iface.ipv4 !== 'N/A')
.map(iface => iface.ipv4);
const displayIP = ips.length > 0 ? ips.join(', ') : 'N/A';
const ipUrl = device.ip_url || (ips.length > 0 ? `http://${ips[0]}` : '');
return `
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
<div style="display: flex; align-items: center; gap: 0.5rem;">
${ipUrl ? `<a href="${utils.escapeHtml(ipUrl)}" target="_blank" rel="noopener noreferrer" style="color: var(--color-info); text-decoration: none; font-weight: 600;" title="Ouvrir ${utils.escapeHtml(ipUrl)}">${utils.escapeHtml(displayIP)}</a>` : `<span>${utils.escapeHtml(displayIP)}</span>`}
<button id="btn-edit-ip-url" class="icon-btn" data-icon="edit" title="Éditer le lien IP" type="button" style="padding: 0.25rem; font-size: 0.75rem;">
<span data-icon="edit"></span>
</button>
</div>
<div id="ip-url-editor" style="display: none;">
<input type="text" id="ip-url-input" class="ip-url-input" placeholder="http://${ips[0] || '10.0.0.1'}" value="${utils.escapeHtml(ipUrl)}" style="width: 100%; padding: 0.5rem; border: 1px solid var(--border-color); border-radius: 4px; background: var(--bg-primary); color: var(--text-primary);">
<div style="display: flex; gap: 0.5rem; margin-top: 0.5rem;">
<button id="btn-save-ip-url" class="btn btn-success btn-sm" data-icon="check" type="button">
<span data-icon="check"></span> Sauvegarder
</button>
<button id="btn-cancel-ip-url" class="btn btn-secondary btn-sm" data-icon="times" type="button">
<span data-icon="times"></span> Annuler
</button>
</div>
</div>
</div>
`;
}
async function editIPUrl() {
const editor = document.getElementById('ip-url-editor');
const btnEdit = document.getElementById('btn-edit-ip-url');
if (!editor || !btnEdit) return;
editor.style.display = 'block';
btnEdit.style.display = 'none';
document.getElementById('ip-url-input')?.focus();
}
async function saveIPUrl() {
if (!currentDevice) return;
const input = document.getElementById('ip-url-input');
if (!input) return;
let url = input.value.trim();
// Auto-prefix http:// if not present and not empty
if (url && !url.match(/^https?:\/\//)) {
url = `http://${url}`;
}
try {
await apiClient.updateDevice(currentDevice.id, { ip_url: url || null });
utils.showToast('Lien IP sauvegardé', 'success');
await reloadCurrentDevice();
} catch (error) {
console.error('Failed to save IP URL:', error);
utils.showToast(error.message || 'Échec de la sauvegarde du lien IP', 'error');
}
}
async function cancelIPUrlEdit() {
if (!currentDevice) return;
const editor = document.getElementById('ip-url-editor');
const btnEdit = document.getElementById('btn-edit-ip-url');
if (!editor || !btnEdit) return;
editor.style.display = 'none';
btnEdit.style.display = 'inline-block';
// Reset input value
const input = document.getElementById('ip-url-input');
if (input) {
input.value = currentDevice.ip_url || '';
}
}
// Search model on web
function searchModelOnWeb() {
const btn = document.getElementById('btn-search-model');
if (!btn) return;
const model = btn.dataset.model;
if (!model || model === 'N/A') {
utils.showToast('Aucun modèle à rechercher', 'warning');
return;
}
// Get search engine from settings (default: Google)
const searchEngine = localStorage.getItem('searchEngine') || 'google';
const searchUrls = {
google: `https://www.google.com/search?q=${encodeURIComponent(model)}`,
duckduckgo: `https://duckduckgo.com/?q=${encodeURIComponent(model)}`,
bing: `https://www.bing.com/search?q=${encodeURIComponent(model)}`
};
const url = searchUrls[searchEngine] || searchUrls.google;
window.open(url, '_blank', 'noopener,noreferrer');
}
// Render device details (right panel)
function renderDeviceDetails(device) {
const previousDeviceId = currentDevice?.id;
@@ -1118,6 +1230,14 @@ function renderDeviceDetails(device) {
<div class="header-meta">${metaParts.length > 0 ? metaParts.join(' • ') : 'Aucune métadonnée'}</div>
</div>
</div>
<div class="header-row">
<div>
<div class="header-label">Adresse IP</div>
<div class="header-value" id="ip-display-container">
${renderIPDisplay(snapshot, device)}
</div>
</div>
</div>
<div class="header-row">
<div>
<div class="header-label">Marque</div>
@@ -1127,7 +1247,12 @@ function renderDeviceDetails(device) {
<div class="header-row">
<div>
<div class="header-label">Modèle</div>
<div class="header-value">${utils.escapeHtml(model)}</div>
<div class="header-value" style="display: flex; align-items: center; gap: 0.5rem;">
<span>${utils.escapeHtml(model)}</span>
<button id="btn-search-model" class="icon-btn" title="Recherche sur le Web" type="button" style="padding: 0.25rem; font-size: 0.75rem;" data-model="${utils.escapeHtml(model)}">
<span data-icon="globe"></span>
</button>
</div>
</div>
</div>
<div class="header-row">
@@ -1291,6 +1416,11 @@ function renderDeviceDetails(device) {
detailsContainer.innerHTML = headerHtml + orderedSections;
// Initialize icons using IconManager
if (window.IconManager) {
window.IconManager.inlineSvgIcons(detailsContainer);
}
bindDetailActions();
loadLinksSection(device.id);
loadBenchmarkHistorySection(device.id);
@@ -1305,6 +1435,14 @@ function bindDetailActions() {
const btnUploadPDF = document.getElementById('btn-upload-pdf');
const btnDelete = document.getElementById('btn-delete');
// IP URL editing
const btnEditIpUrl = document.getElementById('btn-edit-ip-url');
const btnSaveIpUrl = document.getElementById('btn-save-ip-url');
const btnCancelIpUrl = document.getElementById('btn-cancel-ip-url');
// Web search
const btnSearchModel = document.getElementById('btn-search-model');
if (btnEdit) btnEdit.addEventListener('click', toggleEditMode);
if (btnSave) btnSave.addEventListener('click', saveDevice);
if (btnCancel) {
@@ -1317,6 +1455,14 @@ function bindDetailActions() {
if (btnUploadImageHeader) btnUploadImageHeader.addEventListener('click', uploadImage);
if (btnUploadPDF) btnUploadPDF.addEventListener('click', uploadPDF);
if (btnDelete) btnDelete.addEventListener('click', deleteCurrentDevice);
// Bind IP URL actions
if (btnEditIpUrl) btnEditIpUrl.addEventListener('click', editIPUrl);
if (btnSaveIpUrl) btnSaveIpUrl.addEventListener('click', saveIPUrl);
if (btnCancelIpUrl) btnCancelIpUrl.addEventListener('click', cancelIPUrlEdit);
// Bind web search
if (btnSearchModel) btnSearchModel.addEventListener('click', searchModelOnWeb);
}
async function loadLinksSection(deviceId) {

View File

@@ -0,0 +1,579 @@
// Hardware Renderer - Common rendering functions for hardware sections
// Shared between devices.js and device_detail.js to avoid duplication
(function() {
'use strict';
// Get utilities from global scope
const utils = window.BenchUtils;
// Helper: Clean empty/whitespace values
const cleanValue = (val) => {
if (!val || (typeof val === 'string' && val.trim() === '')) return 'N/A';
return val;
};
// Helper: Render no data message
const noData = (message = 'Aucune information disponible') => {
return `<p style="color: var(--text-muted); text-align: center; padding: 2rem;">${message}</p>`;
};
// =======================
// MOTHERBOARD SECTION
// =======================
function renderMotherboardDetails(snapshot) {
if (!snapshot) return noData();
const items = [
{ label: 'Fabricant', value: cleanValue(snapshot.motherboard_vendor || snapshot.system_vendor) },
{ label: 'Modèle', value: cleanValue(snapshot.motherboard_model || snapshot.system_model) },
{ label: 'BIOS fabricant', value: cleanValue(snapshot.bios_vendor) },
{ label: 'Version BIOS', value: cleanValue(snapshot.bios_version) },
{ label: 'Date BIOS', value: cleanValue(snapshot.bios_date) },
{ label: 'Hostname', value: cleanValue(snapshot.hostname) },
{ label: 'Slots RAM', value: (snapshot.ram_slots_used != null || snapshot.ram_slots_total != null) ? `${snapshot.ram_slots_used ?? '?'} / ${snapshot.ram_slots_total ?? '?'}` : 'N/A' },
{ label: 'Famille', value: cleanValue(snapshot.system_family) },
{ label: 'Châssis', value: cleanValue(snapshot.chassis_type) }
];
return `
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.75rem;">
${items.map(item => `
<div style="padding: 0.75rem; background: var(--bg-primary); border-radius: 6px; border: 1px solid var(--border-color);">
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 0.25rem;">${item.label}</div>
<div style="font-weight: 600; color: var(--text-primary);">${utils.escapeHtml(String(item.value))}</div>
</div>
`).join('')}
</div>
`;
}
// =======================
// CPU SECTION
// =======================
function renderCPUDetails(snapshot) {
if (!snapshot) return noData();
// Parse multi-CPU from raw_info if available
const rawInfo = snapshot.raw_info_json ? (typeof snapshot.raw_info_json === 'string' ? JSON.parse(snapshot.raw_info_json) : snapshot.raw_info_json) : null;
const dmidecode = rawInfo?.dmidecode || '';
// Check for multi-socket CPUs (Proc 1, Proc 2, etc.)
const cpuSockets = [];
const socketRegex = /Handle 0x[0-9A-F]+, DMI type 4[\s\S]*?Socket Designation: (.*?)[\s\S]*?Version: (.*?)[\s\S]*?Core Count: (\d+)[\s\S]*?Thread Count: (\d+)[\s\S]*?(?:Max Speed: (\d+) MHz)?[\s\S]*?(?:Current Speed: (\d+) MHz)?[\s\S]*?(?:Voltage: ([\d.]+ V))?/g;
let match;
while ((match = socketRegex.exec(dmidecode)) !== null) {
cpuSockets.push({
socket: match[1].trim(),
model: match[2].trim(),
cores: match[3],
threads: match[4],
maxSpeed: match[5] ? `${match[5]} MHz` : 'N/A',
currentSpeed: match[6] ? `${match[6]} MHz` : 'N/A',
voltage: match[7] || 'N/A'
});
}
// Parse CPU signature (Family, Model, Stepping)
const signatureRegex = /Signature: Type \d+, Family (\d+), Model (\d+), Stepping (\d+)/;
const sigMatch = dmidecode.match(signatureRegex);
const cpuSignature = sigMatch ? `Family ${sigMatch[1]}, Model ${sigMatch[2]}, Stepping ${sigMatch[3]}` : 'N/A';
const items = [
{ label: 'Fabricant', value: snapshot.cpu_vendor || 'N/A', tooltip: snapshot.cpu_model },
{ label: 'Modèle', value: snapshot.cpu_model || 'N/A', tooltip: snapshot.cpu_microarchitecture ? `Architecture: ${snapshot.cpu_microarchitecture}` : null },
{ label: 'Signature CPU', value: cpuSignature },
{ label: 'Socket', value: cpuSockets.length > 0 ? cpuSockets[0].socket : 'N/A' },
{ label: 'Famille', value: snapshot.cpu_vendor || 'N/A' },
{ label: 'Microarchitecture', value: snapshot.cpu_microarchitecture || 'N/A' },
{ label: 'Cores', value: snapshot.cpu_cores != null ? snapshot.cpu_cores : 'N/A', tooltip: snapshot.cpu_threads ? `${snapshot.cpu_threads} threads disponibles` : null },
{ label: 'Threads', value: snapshot.cpu_threads != null ? snapshot.cpu_threads : 'N/A', tooltip: snapshot.cpu_cores ? `${snapshot.cpu_cores} cores physiques` : null },
{ label: 'Fréquence de base', value: snapshot.cpu_base_freq_ghz ? `${snapshot.cpu_base_freq_ghz} GHz` : 'N/A', tooltip: snapshot.cpu_max_freq_ghz ? `Max: ${snapshot.cpu_max_freq_ghz} GHz` : null },
{ label: 'Fréquence maximale', value: snapshot.cpu_max_freq_ghz ? `${snapshot.cpu_max_freq_ghz} GHz` : (cpuSockets.length > 0 && cpuSockets[0].maxSpeed !== 'N/A' ? cpuSockets[0].maxSpeed : 'N/A') },
{ label: 'Fréquence actuelle', value: cpuSockets.length > 0 && cpuSockets[0].currentSpeed !== 'N/A' ? cpuSockets[0].currentSpeed : 'N/A' },
{ label: 'Tension', value: cpuSockets.length > 0 ? cpuSockets[0].voltage : 'N/A' },
{ label: 'TDP', value: snapshot.cpu_tdp_w ? `${snapshot.cpu_tdp_w} W` : 'N/A', tooltip: 'Thermal Design Power - Consommation thermique typique' },
{ label: 'Cache L1', value: snapshot.cpu_cache_l1_kb ? utils.formatCache(snapshot.cpu_cache_l1_kb) : 'N/A', tooltip: 'Cache de niveau 1 - Le plus rapide' },
{ label: 'Cache L2', value: snapshot.cpu_cache_l2_kb ? utils.formatCache(snapshot.cpu_cache_l2_kb) : 'N/A', tooltip: 'Cache de niveau 2 - Intermédiaire' },
{ label: 'Cache L3', value: snapshot.cpu_cache_l3_kb ? utils.formatCache(snapshot.cpu_cache_l3_kb) : 'N/A', tooltip: 'Cache de niveau 3 - Partagé entre les cores' }
];
let html = `
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.75rem;">
${items.map(item => `
<div style="padding: 0.75rem; background: var(--bg-primary); border-radius: 6px; border: 1px solid var(--border-color);">
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 0.25rem;">${item.label}</div>
<div style="font-weight: 600; color: var(--text-primary);" ${item.tooltip ? `title="${utils.escapeHtml(item.tooltip)}"` : ''}>${utils.escapeHtml(String(item.value))}</div>
</div>
`).join('')}
</div>
`;
// Multi-CPU grid
if (cpuSockets.length > 1) {
html += `
<div style="margin-top: 1rem; padding: 0.75rem; background: var(--bg-secondary); border-radius: 6px; border: 1px solid var(--border-color);">
<div style="font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);">Configuration multi-CPU (${cpuSockets.length} sockets)</div>
<div style="overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse; font-size: 0.85rem;">
<thead>
<tr style="background: var(--bg-primary); border-bottom: 2px solid var(--border-color);">
<th style="padding: 0.5rem; text-align: left;">Socket</th>
<th style="padding: 0.5rem; text-align: left;">Modèle</th>
<th style="padding: 0.5rem; text-align: center;">Cores</th>
<th style="padding: 0.5rem; text-align: center;">Threads</th>
<th style="padding: 0.5rem; text-align: center;">Fréq. Max</th>
<th style="padding: 0.5rem; text-align: center;">Fréq. Actuelle</th>
<th style="padding: 0.5rem; text-align: center;">Tension</th>
</tr>
</thead>
<tbody>
${cpuSockets.map((cpu, idx) => `
<tr style="border-bottom: 1px solid var(--border-color); ${idx % 2 === 0 ? 'background: var(--bg-primary);' : ''}">
<td style="padding: 0.5rem;">${utils.escapeHtml(cpu.socket)}</td>
<td style="padding: 0.5rem;">${utils.escapeHtml(cpu.model)}</td>
<td style="padding: 0.5rem; text-align: center;">${cpu.cores}</td>
<td style="padding: 0.5rem; text-align: center;">${cpu.threads}</td>
<td style="padding: 0.5rem; text-align: center;">${cpu.maxSpeed}</td>
<td style="padding: 0.5rem; text-align: center;">${cpu.currentSpeed}</td>
<td style="padding: 0.5rem; text-align: center;">${cpu.voltage}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
`;
}
// CPU flags
if (snapshot.cpu_flags) {
let flags = snapshot.cpu_flags;
if (typeof flags === 'string') {
try {
flags = JSON.parse(flags);
} catch (error) {
flags = flags.split(',').map(flag => flag.trim()).filter(Boolean);
}
}
if (!Array.isArray(flags)) {
flags = [];
}
const limitedFlags = flags.slice(0, 20); // Limit to 20
html += `
<div style="margin-top: 1rem;">
<div style="font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 0.5rem;">Extensions CPU (${flags.length} total)</div>
<div style="display: flex; flex-wrap: wrap; gap: 0.35rem;">
${limitedFlags.map(flag => `
<span style="padding: 0.2rem 0.5rem; background: var(--bg-secondary); border-radius: 3px; font-size: 0.75rem; color: var(--text-primary); border: 1px solid var(--border-color);">
${utils.escapeHtml(flag)}
</span>
`).join('')}
${flags.length > 20 ? `<span style="padding: 0.2rem 0.5rem; color: var(--text-secondary); font-size: 0.75rem;">+${flags.length - 20} autres...</span>` : ''}
</div>
</div>
`;
}
return html;
}
// =======================
// MEMORY SECTION
// =======================
function renderMemoryDetails(snapshot, deviceData) {
if (!snapshot) return noData();
// Parse RAM layout
const ramLayout = snapshot.ram_layout_json ?
(typeof snapshot.ram_layout_json === 'string' ? JSON.parse(snapshot.ram_layout_json) : snapshot.ram_layout_json) :
[];
const slotsUsed = ramLayout.filter(slot => slot.size_mb > 0).length;
const slotsTotal = snapshot.ram_slots_total || ramLayout.length || 0;
// ECC detection
const hasECC = ramLayout.some(slot => slot.type_detail && slot.type_detail.toLowerCase().includes('ecc'));
// RAM bars data
const ramTotal = snapshot.ram_total_mb || 0;
const ramFree = snapshot.ram_free_mb || 0;
const ramUsed = ramTotal - ramFree;
const ramShared = snapshot.ram_shared_mb || 0;
const ramUsedPercent = ramTotal > 0 ? Math.round((ramUsed / ramTotal) * 100) : 0;
const swapTotal = snapshot.swap_total_mb || 0;
const swapUsed = snapshot.swap_used_mb || 0;
const swapPercent = swapTotal > 0 ? Math.round((swapUsed / swapTotal) * 100) : 0;
const cards = [
{ label: 'Capacité max carte mère', value: snapshot.ram_max_capacity_mb ? `${Math.round(snapshot.ram_max_capacity_mb / 1024)} GB` : 'N/A' },
{ label: 'RAM Totale', value: utils.formatStorage(ramTotal, 'MB') },
{ label: 'RAM Libre', value: utils.formatStorage(ramFree, 'MB') },
{ label: 'RAM Utilisée', value: utils.formatStorage(ramUsed, 'MB') },
{ label: 'RAM Partagée', value: utils.formatStorage(ramShared, 'MB') },
{ label: 'Slots utilisés / total', value: `${slotsUsed} / ${slotsTotal}` },
{ label: 'ECC', value: hasECC ? 'Oui' : 'Non' }
];
let html = `
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 0.75rem; margin-bottom: 1rem;">
${cards.map(card => `
<div style="padding: 0.75rem; background: var(--bg-primary); border-radius: 6px; border: 1px solid var(--border-color);">
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 0.25rem;">${card.label}</div>
<div style="font-weight: 600; color: var(--text-primary);">${card.value}</div>
</div>
`).join('')}
</div>
`;
// RAM bar
html += `
<div style="margin-bottom: 1rem;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.25rem; font-size: 0.85rem;">
<span style="font-weight: 600;">RAM (${utils.formatStorage(ramTotal, 'MB')})</span>
<span>${ramUsedPercent}% utilisée</span>
</div>
<div style="width: 100%; height: 24px; background: var(--bg-tertiary); border-radius: 4px; overflow: hidden; border: 1px solid var(--border-color); position: relative;">
<div style="position: absolute; top: 0; left: 0; height: 100%; width: ${ramUsedPercent}%; background: linear-gradient(to right, var(--color-warning), var(--color-danger)); transition: width 0.3s;"></div>
</div>
<div style="font-size: 0.75rem; color: var(--text-secondary); margin-top: 0.25rem; display: flex; gap: 1rem;">
<span>▮ Utilisée: ${utils.formatStorage(ramUsed, 'MB')}</span>
<span>▯ Disponible: ${utils.formatStorage(ramFree, 'MB')}</span>
</div>
</div>
`;
// SWAP bar
if (swapTotal > 0) {
html += `
<div style="margin-bottom: 1rem;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.25rem; font-size: 0.85rem;">
<span style="font-weight: 600;">SWAP (${utils.formatStorage(swapTotal, 'MB')})</span>
<span>${swapPercent}% utilisé</span>
</div>
<div style="width: 100%; height: 20px; background: var(--bg-tertiary); border-radius: 4px; overflow: hidden; border: 1px solid var(--border-color); position: relative;">
<div style="position: absolute; top: 0; left: 0; height: 100%; width: ${swapPercent}%; background: var(--color-info); transition: width 0.3s;"></div>
</div>
<div style="font-size: 0.75rem; color: var(--text-secondary); margin-top: 0.25rem;">
▮ Utilisé: ${utils.formatStorage(swapUsed, 'MB')} | ▯ Libre: ${utils.formatStorage(swapTotal - swapUsed, 'MB')}
</div>
</div>
`;
}
// Memory slots
if (ramLayout && ramLayout.length > 0) {
html += `<div class="memory-slots-grid">`;
ramLayout.forEach((slot, idx) => {
const slotName = slot.slot || slot.locator || `DIMM${idx}`;
const sizeMB = slot.size_mb || 0;
const sizeGB = sizeMB > 0 ? Math.round(sizeMB / 1024) : 0;
const status = sizeMB > 0 ? 'occupé' : 'libre';
const type = slot.type || 'N/A';
const speed = slot.speed_mhz ? `${slot.speed_mhz} MT/s` : 'N/A';
const typeDetail = slot.type_detail || 'N/A';
const formFactor = slot.form_factor || 'N/A';
const voltage = slot.voltage_v ? `${slot.voltage_v} V` : 'N/A';
const manufacturer = slot.manufacturer || 'N/A';
const serialNumber = slot.serial_number || 'N/A';
const partNumber = slot.part_number || 'N/A';
html += `
<div class="memory-slot ${sizeMB > 0 ? 'occupied' : 'empty'}" data-slot-index="${idx}">
<div class="memory-slot-header">
<span class="memory-slot-name">${utils.escapeHtml(slotName)}</span>
<span class="memory-slot-size">${sizeGB > 0 ? sizeGB + 'GB' : ''}</span>
<span class="memory-slot-status">${status}</span>
</div>
${sizeMB > 0 ? `
<div class="memory-slot-details">
<div class="memory-slot-main">${utils.escapeHtml(type)} ${utils.escapeHtml(speed)} | ${utils.escapeHtml(typeDetail)}</div>
<div class="memory-slot-sub">${utils.escapeHtml(formFactor)} | ${utils.escapeHtml(voltage)} | ${utils.escapeHtml(manufacturer)}</div>
<div class="memory-slot-tiny">SN: ${utils.escapeHtml(serialNumber)}</div>
<div class="memory-slot-tiny">PN: ${utils.escapeHtml(partNumber)}</div>
</div>
` : ''}
</div>
`;
});
html += `</div>`;
}
return html;
}
// =======================
// STORAGE SECTION
// =======================
function renderStorageDetails(snapshot) {
if (!snapshot) return noData();
const storageDevices = snapshot.storage_devices_json ?
(typeof snapshot.storage_devices_json === 'string' ? JSON.parse(snapshot.storage_devices_json) : snapshot.storage_devices_json) :
[];
if (!storageDevices || storageDevices.length === 0) {
return noData('Aucun périphérique de stockage détecté');
}
return storageDevices.map(device => {
const name = device.name || 'N/A';
const model = device.model || 'N/A';
const size = device.size_gb ? `${device.size_gb} GB` : 'N/A';
const type = device.type || 'N/A';
const smart = device.smart_status || 'N/A';
const temp = device.temperature_c != null ? `${device.temperature_c}°C` : 'N/A';
const smartColor = smart.toLowerCase().includes('passed') || smart.toLowerCase().includes('ok') ? 'var(--color-success)' :
smart.toLowerCase().includes('fail') ? 'var(--color-danger)' :
'var(--text-secondary)';
return `
<div style="padding: 1rem; background: var(--bg-primary); border-radius: 6px; border: 1px solid var(--border-color); margin-bottom: 0.75rem;">
<div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem;">
<div style="font-size: 2rem;">${type.toLowerCase().includes('ssd') ? '💾' : '🗄️'}</div>
<div style="flex: 1;">
<div style="font-weight: 600; font-size: 1rem; color: var(--text-primary);">${utils.escapeHtml(name)}</div>
<div style="font-size: 0.85rem; color: var(--text-secondary);">${utils.escapeHtml(model)}</div>
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 0.5rem;">
<div>
<div style="font-size: 0.75rem; color: var(--text-secondary);">Capacité</div>
<div style="font-weight: 600; color: var(--text-primary);">${size}</div>
</div>
<div>
<div style="font-size: 0.75rem; color: var(--text-secondary);">Type</div>
<div style="font-weight: 600; color: var(--text-primary);">${utils.escapeHtml(type)}</div>
</div>
<div>
<div style="font-size: 0.75rem; color: var(--text-secondary);">SMART</div>
<div style="font-weight: 600; color: ${smartColor};">${utils.escapeHtml(smart)}</div>
</div>
<div>
<div style="font-size: 0.75rem; color: var(--text-secondary);">Température</div>
<div style="font-weight: 600; color: var(--text-primary);">${temp}</div>
</div>
</div>
</div>
`;
}).join('');
}
// =======================
// GPU SECTION
// =======================
function renderGPUDetails(snapshot) {
if (!snapshot) return noData();
const items = [
{ label: 'Fabricant', value: snapshot.gpu_vendor || 'N/A' },
{ label: 'Modèle', value: snapshot.gpu_model || 'N/A' },
{ label: 'VRAM', value: snapshot.gpu_vram_mb ? `${snapshot.gpu_vram_mb} MB` : 'N/A' },
{ label: 'Driver', value: snapshot.gpu_driver || 'N/A' }
];
return `
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.75rem;">
${items.map(item => `
<div style="padding: 0.75rem; background: var(--bg-primary); border-radius: 6px; border: 1px solid var(--border-color);">
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 0.25rem;">${item.label}</div>
<div style="font-weight: 600; color: var(--text-primary);">${utils.escapeHtml(String(item.value))}</div>
</div>
`).join('')}
</div>
`;
}
// =======================
// NETWORK SECTION
// =======================
function renderNetworkDetails(snapshot) {
if (!snapshot) return noData();
const networkInterfaces = snapshot.network_interfaces_json ?
(typeof snapshot.network_interfaces_json === 'string' ? JSON.parse(snapshot.network_interfaces_json) : snapshot.network_interfaces_json) :
[];
if (!networkInterfaces || networkInterfaces.length === 0) {
return noData('Aucune interface réseau détectée');
}
return networkInterfaces.map(iface => {
const name = iface.name || 'N/A';
const ipv4 = iface.ipv4 || 'N/A';
const ipv6 = iface.ipv6 || 'N/A';
const mac = iface.mac || 'N/A';
const speed = iface.speed_mbps ? `${iface.speed_mbps} Mbps` : 'N/A';
const status = iface.status || 'N/A';
const statusColor = status.toLowerCase().includes('up') ? 'var(--color-success)' :
status.toLowerCase().includes('down') ? 'var(--color-danger)' :
'var(--text-secondary)';
return `
<div style="padding: 1rem; background: var(--bg-primary); border-radius: 6px; border: 1px solid var(--border-color); margin-bottom: 0.75rem;">
<div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem;">
<div style="font-size: 2rem;">🌐</div>
<div style="flex: 1;">
<div style="font-weight: 600; font-size: 1rem; color: var(--text-primary);">${utils.escapeHtml(name)}</div>
<div style="font-size: 0.85rem; color: var(--text-secondary);">${utils.escapeHtml(mac)}</div>
</div>
<div style="font-weight: 600; color: ${statusColor};">${utils.escapeHtml(status)}</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 0.5rem;">
<div>
<div style="font-size: 0.75rem; color: var(--text-secondary);">IPv4</div>
<div style="font-weight: 600; color: var(--text-primary); font-family: monospace; font-size: 0.85rem;">${utils.escapeHtml(ipv4)}</div>
</div>
<div>
<div style="font-size: 0.75rem; color: var(--text-secondary);">IPv6</div>
<div style="font-weight: 600; color: var(--text-primary); font-family: monospace; font-size: 0.75rem; word-break: break-all;">${utils.escapeHtml(ipv6)}</div>
</div>
<div>
<div style="font-size: 0.75rem; color: var(--text-secondary);">Vitesse</div>
<div style="font-weight: 600; color: var(--text-primary);">${speed}</div>
</div>
</div>
</div>
`;
}).join('');
}
// =======================
// OS SECTION
// =======================
function renderOSDetails(snapshot) {
if (!snapshot) return noData();
const items = [
{ label: 'OS', 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: 'Hostname', value: snapshot.hostname || 'N/A' },
{ label: 'Uptime', value: snapshot.uptime_seconds ? utils.formatUptime(snapshot.uptime_seconds) : 'N/A' },
{ label: 'Batterie', value: snapshot.battery_percent != null ? `${snapshot.battery_percent}%` : 'N/A' }
];
return `
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.75rem;">
${items.map(item => `
<div style="padding: 0.75rem; background: var(--bg-primary); border-radius: 6px; border: 1px solid var(--border-color);">
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 0.25rem;">${item.label}</div>
<div style="font-weight: 600; color: var(--text-primary);">${utils.escapeHtml(String(item.value))}</div>
</div>
`).join('')}
</div>
`;
}
// =======================
// PROXMOX SECTION
// =======================
function renderProxmoxDetails(snapshot) {
if (!snapshot) return noData();
const isProxmoxHost = snapshot.is_proxmox_host;
const isProxmoxGuest = snapshot.is_proxmox_guest;
const proxmoxVersion = snapshot.proxmox_version;
if (!isProxmoxHost && !isProxmoxGuest) {
return noData('Non détecté comme hôte ou invité Proxmox');
}
const items = [
{ label: 'Type', value: isProxmoxHost ? 'Hôte Proxmox' : 'Invité Proxmox' },
{ label: 'Version', value: proxmoxVersion || 'N/A' }
];
return `
<div style="padding: 1rem; background: var(--bg-primary); border-radius: 6px; border: 1px solid var(--color-info);">
<div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem;">
<div style="font-size: 2rem;">🔧</div>
<div style="font-weight: 600; font-size: 1.1rem; color: var(--color-info);">Proxmox VE détecté</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.75rem;">
${items.map(item => `
<div style="padding: 0.75rem; background: var(--bg-secondary); border-radius: 6px; border: 1px solid var(--border-color);">
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 0.25rem;">${item.label}</div>
<div style="font-weight: 600; color: var(--text-primary);">${utils.escapeHtml(String(item.value))}</div>
</div>
`).join('')}
</div>
</div>
`;
}
// =======================
// AUDIO SECTION
// =======================
function renderAudioDetails(snapshot) {
if (!snapshot) return noData();
const audioHardware = snapshot.audio_hardware_json ?
(typeof snapshot.audio_hardware_json === 'string' ? JSON.parse(snapshot.audio_hardware_json) : snapshot.audio_hardware_json) :
null;
const audioSoftware = snapshot.audio_software_json ?
(typeof snapshot.audio_software_json === 'string' ? JSON.parse(snapshot.audio_software_json) : snapshot.audio_software_json) :
null;
if (!audioHardware && !audioSoftware) {
return noData('Aucune information audio disponible');
}
let html = '';
// Hardware section
if (audioHardware && Array.isArray(audioHardware) && audioHardware.length > 0) {
html += `
<div style="margin-bottom: 1rem;">
<div style="font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);">🔊 Matériel Audio</div>
${audioHardware.map(device => `
<div style="padding: 0.75rem; background: var(--bg-primary); border-radius: 6px; border: 1px solid var(--border-color); margin-bottom: 0.5rem;">
<div style="font-weight: 600; color: var(--text-primary);">${utils.escapeHtml(device.name || 'N/A')}</div>
<div style="font-size: 0.85rem; color: var(--text-secondary);">${utils.escapeHtml(device.driver || 'N/A')}</div>
</div>
`).join('')}
</div>
`;
}
// Software section
if (audioSoftware) {
html += `
<div>
<div style="font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);">🎵 Logiciels Audio</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.75rem;">
${Object.entries(audioSoftware).map(([key, value]) => `
<div style="padding: 0.75rem; background: var(--bg-primary); border-radius: 6px; border: 1px solid var(--border-color);">
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 0.25rem;">${utils.escapeHtml(key)}</div>
<div style="font-weight: 600; color: var(--text-primary);">${utils.escapeHtml(String(value))}</div>
</div>
`).join('')}
</div>
</div>
`;
}
return html;
}
// =======================
// EXPORT PUBLIC API
// =======================
window.HardwareRenderer = {
renderMotherboardDetails,
renderCPUDetails,
renderMemoryDetails,
renderStorageDetails,
renderGPUDetails,
renderNetworkDetails,
renderOSDetails,
renderProxmoxDetails,
renderAudioDetails
};
})();

251
frontend/js/icon-manager.js Normal file
View File

@@ -0,0 +1,251 @@
/**
* Icon Manager - Gestion des packs d'icônes
* Permet de basculer entre emojis, FontAwesome, et icônes personnalisées
*/
(function() {
'use strict';
const ICON_PACKS = {
'emoji': {
name: 'Emojis Unicode',
description: 'Emojis colorés par défaut',
icons: {
'add': '',
'edit': '✏️',
'delete': '🗑️',
'save': '💾',
'upload': '📤',
'download': '📥',
'image': '🖼️',
'file': '📄',
'pdf': '📕',
'link': '🔗',
'refresh': '🔄',
'search': '🌍',
'settings': '⚙️',
'close': '❌',
'check': '✅',
'warning': '⚠️',
'info': '',
'copy': '📋'
}
},
'fontawesome-solid': {
name: 'FontAwesome Solid',
description: 'Icônes FontAwesome pleines (bold)',
icons: {
'add': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/plus.svg" aria-hidden="true"></span>',
'edit': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/pen-to-square.svg" aria-hidden="true"></span>',
'delete': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/trash-can.svg" aria-hidden="true"></span>',
'save': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/floppy-disk.svg" aria-hidden="true"></span>',
'upload': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/upload.svg" aria-hidden="true"></span>',
'download': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/download.svg" aria-hidden="true"></span>',
'image': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/image.svg" aria-hidden="true"></span>',
'file': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/file.svg" aria-hidden="true"></span>',
'pdf': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/file-pdf.svg" aria-hidden="true"></span>',
'link': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/link.svg" aria-hidden="true"></span>',
'refresh': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/arrows-rotate.svg" aria-hidden="true"></span>',
'search': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/earth-europe.svg" aria-hidden="true"></span>',
'settings': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/gear.svg" aria-hidden="true"></span>',
'close': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/xmark.svg" aria-hidden="true"></span>',
'check': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/check.svg" aria-hidden="true"></span>',
'warning': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/triangle-exclamation.svg" aria-hidden="true"></span>',
'info': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/circle-info.svg" aria-hidden="true"></span>',
'copy': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/copy.svg" aria-hidden="true"></span>'
}
},
'fontawesome-regular': {
name: 'FontAwesome Regular',
description: 'Icônes FontAwesome fines (outline)',
icons: {
'add': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/regular/square-plus.svg" aria-hidden="true"></span>',
'edit': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/regular/pen-to-square.svg" aria-hidden="true"></span>',
'delete': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/regular/trash-can.svg" aria-hidden="true"></span>',
'save': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/regular/floppy-disk.svg" aria-hidden="true"></span>',
'upload': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/upload.svg" aria-hidden="true"></span>',
'download': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/download.svg" aria-hidden="true"></span>',
'image': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/regular/image.svg" aria-hidden="true"></span>',
'file': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/regular/file.svg" aria-hidden="true"></span>',
'pdf': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/regular/file-pdf.svg" aria-hidden="true"></span>',
'link': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/link.svg" aria-hidden="true"></span>',
'refresh': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/arrows-rotate.svg" aria-hidden="true"></span>',
'search': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/earth-europe.svg" aria-hidden="true"></span>',
'settings': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/gear.svg" aria-hidden="true"></span>',
'close': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/regular/circle-xmark.svg" aria-hidden="true"></span>',
'check': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/regular/circle-check.svg" aria-hidden="true"></span>',
'warning': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/triangle-exclamation.svg" aria-hidden="true"></span>',
'info': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/circle-info.svg" aria-hidden="true"></span>',
'copy': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/regular/copy.svg" aria-hidden="true"></span>'
}
},
'icons8': {
name: 'Icons8 PNG',
description: 'Icônes Icons8 existantes (PNG)',
icons: {
'add': '<img src="icons/icons8-done-48.png" class="btn-icon" alt="Add">',
'edit': '<img src="icons/icons8-edit-pencil-48.png" class="btn-icon" alt="Edit">',
'delete': '<img src="icons/icons8-delete-48.png" class="btn-icon" alt="Delete">',
'save': '<img src="icons/icons8-save-48.png" class="btn-icon" alt="Save">',
'upload': '📤',
'download': '📥',
'image': '<img src="icons/icons8-picture-48.png" class="btn-icon" alt="Image">',
'file': '📄',
'pdf': '📕',
'link': '🔗',
'refresh': '🔄',
'search': '🌍',
'settings': '<img src="icons/icons8-setting-48.png" class="btn-icon" alt="Settings">',
'close': '<img src="icons/icons8-close-48.png" class="btn-icon" alt="Close">',
'check': '<img src="icons/icons8-check-mark-48.png" class="btn-icon" alt="Check">',
'warning': '⚠️',
'info': '',
'copy': '📋'
}
}
};
const DEFAULT_PACK = 'emoji';
const STORAGE_KEY = 'benchtools_icon_pack';
const svgCache = new Map();
function normalizeSvg(svgText) {
let text = svgText;
text = text.replace(/fill="(?!none)[^"]*"/g, 'fill="currentColor"');
text = text.replace(/stroke="(?!none)[^"]*"/g, 'stroke="currentColor"');
return text;
}
function inlineSvgElement(el) {
const src = el.getAttribute('data-svg-src');
if (!src) return;
const cached = svgCache.get(src);
if (cached) {
el.innerHTML = cached;
return;
}
fetch(src)
.then(response => {
if (!response.ok) {
throw new Error(`SVG load failed: ${response.status}`);
}
return response.text();
})
.then(text => {
const normalized = normalizeSvg(text);
svgCache.set(src, normalized);
el.innerHTML = normalized;
})
.catch(error => {
console.warn('[IconManager] Failed to inline SVG:', src, error);
});
}
function inlineSvgIcons(root = document) {
const elements = root.querySelectorAll('[data-svg-src]');
elements.forEach(el => inlineSvgElement(el));
}
// Icon Manager Object
const IconManager = {
packs: ICON_PACKS,
getCurrentPack: function() {
return localStorage.getItem(STORAGE_KEY) || DEFAULT_PACK;
},
applyPack: function(packName) {
if (!ICON_PACKS[packName]) {
console.error(`Icon pack "${packName}" not found`);
return false;
}
localStorage.setItem(STORAGE_KEY, packName);
// Dispatch custom event for icon pack change
window.dispatchEvent(new CustomEvent('iconPackChanged', {
detail: {
pack: packName,
packName: ICON_PACKS[packName].name
}
}));
return true;
},
getIcon: function(iconName, fallback = '?') {
const currentPack = this.getCurrentPack();
const pack = ICON_PACKS[currentPack];
if (!pack) {
console.warn(`Icon pack "${currentPack}" not found`);
return fallback;
}
return pack.icons[iconName] || fallback;
},
getAllPacks: function() {
return Object.keys(ICON_PACKS);
},
getPackInfo: function(packName) {
return ICON_PACKS[packName] || null;
},
// Helper pour générer un bouton avec icône
createButton: function(iconName, text = '', className = 'btn btn-primary') {
const icon = this.getIcon(iconName);
const textPart = text ? ` ${text}` : '';
return `<button class="${className}">${icon}${textPart}</button>`;
},
// Helper pour mettre à jour tous les boutons de la page
updateAllButtons: function() {
// Cette fonction sera appelée après changement de pack
// Pour mettre à jour dynamiquement tous les boutons
const buttons = document.querySelectorAll('[data-icon]');
buttons.forEach(btn => {
const iconName = btn.getAttribute('data-icon');
const iconSpan = btn.querySelector('.btn-icon-wrapper');
if (iconSpan && iconName) {
iconSpan.innerHTML = this.getIcon(iconName);
}
});
inlineSvgIcons();
},
inlineSvgIcons: function(root = document) {
inlineSvgIcons(root);
}
};
// Auto-initialize on load
function initializeIcons() {
IconManager.updateAllButtons();
// Also call utils.js initializeButtonIcons if available
if (window.initializeButtonIcons) {
window.initializeButtonIcons();
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeIcons);
} else {
initializeIcons();
}
// Re-initialize when icon pack changes
window.addEventListener('iconPackChanged', function() {
setTimeout(initializeIcons, 100); // Small delay to ensure DOM is ready
});
// Expose globally
window.IconManager = IconManager;
// Log initialization
console.log(`[IconManager] Initialized with pack: ${IconManager.getCurrentPack()}`);
})();

View File

@@ -26,6 +26,8 @@ async function loadBackendConfig() {
document.addEventListener('DOMContentLoaded', async () => {
loadDisplayPreferences();
loadSettings();
loadTheme();
loadIconPack();
await loadBackendConfig();
});
@@ -37,6 +39,7 @@ function loadDisplayPreferences() {
const temperatureUnit = localStorage.getItem('displayPref_temperatureUnit') || 'C';
const sectionIconSize = localStorage.getItem('displayPref_sectionIconSize') || '32';
const buttonIconSize = localStorage.getItem('displayPref_buttonIconSize') || '24';
const searchEngine = localStorage.getItem('searchEngine') || 'google';
document.getElementById('memoryUnit').value = memoryUnit;
document.getElementById('storageUnit').value = storageUnit;
@@ -44,6 +47,7 @@ function loadDisplayPreferences() {
document.getElementById('temperatureUnit').value = temperatureUnit;
document.getElementById('sectionIconSize').value = sectionIconSize;
document.getElementById('buttonIconSize').value = buttonIconSize;
document.getElementById('searchEngine').value = searchEngine;
// Apply icon sizes
applyIconSizes(sectionIconSize, buttonIconSize);
@@ -63,6 +67,7 @@ function saveDisplayPreferences() {
const temperatureUnit = document.getElementById('temperatureUnit').value;
const sectionIconSize = document.getElementById('sectionIconSize').value;
const buttonIconSize = document.getElementById('buttonIconSize').value;
const searchEngine = document.getElementById('searchEngine').value;
localStorage.setItem('displayPref_memoryUnit', memoryUnit);
localStorage.setItem('displayPref_storageUnit', storageUnit);
@@ -70,6 +75,7 @@ function saveDisplayPreferences() {
localStorage.setItem('displayPref_temperatureUnit', temperatureUnit);
localStorage.setItem('displayPref_sectionIconSize', sectionIconSize);
localStorage.setItem('displayPref_buttonIconSize', buttonIconSize);
localStorage.setItem('searchEngine', searchEngine);
// Apply icon sizes immediately
applyIconSizes(sectionIconSize, buttonIconSize);
@@ -226,6 +232,80 @@ async function copyToken() {
}
}
// ==========================================
// THEME MANAGEMENT
// ==========================================
function loadTheme() {
const currentTheme = window.ThemeManager ? window.ThemeManager.getCurrentTheme() : 'monokai-dark';
const select = document.getElementById('themeSelect');
if (select) {
select.value = currentTheme;
}
}
function saveTheme() {
const select = document.getElementById('themeSelect');
if (!select) return;
const theme = select.value;
if (window.ThemeManager) {
window.ThemeManager.applyTheme(theme);
showToast(`Thème "${theme}" appliqué avec succès`, 'success');
} else {
console.error('ThemeManager not available');
showToast('Erreur: ThemeManager non disponible', 'error');
}
}
// ==========================================
// ICON PACK MANAGEMENT
// ==========================================
function loadIconPack() {
const currentPack = window.IconManager ? window.IconManager.getCurrentPack() : 'fontawesome-regular';
const select = document.getElementById('iconPackSelect');
if (select) {
select.value = currentPack;
}
// Initialize icon preview
if (window.IconManager) {
const preview = document.getElementById('iconPreview');
if (preview) {
window.IconManager.inlineSvgIcons(preview);
}
}
}
function saveIconPack() {
const select = document.getElementById('iconPackSelect');
if (!select) return;
const pack = select.value;
if (window.IconManager) {
const success = window.IconManager.applyPack(pack);
if (success) {
showToast(`Pack d'icônes "${pack}" appliqué avec succès`, 'success');
// Refresh preview
const preview = document.getElementById('iconPreview');
if (preview) {
setTimeout(() => {
window.IconManager.inlineSvgIcons(preview);
}, 100);
}
} else {
showToast('Erreur lors de l\'application du pack d\'icônes', 'error');
}
} else {
console.error('IconManager not available');
showToast('Erreur: IconManager non disponible', 'error');
}
}
// Make functions available globally
window.generateBenchCommand = generateBenchCommand;
window.copyGeneratedCommand = copyGeneratedCommand;
@@ -233,3 +313,5 @@ window.toggleTokenVisibility = toggleTokenVisibility;
window.copyToken = copyToken;
window.saveDisplayPreferences = saveDisplayPreferences;
window.resetDisplayPreferences = resetDisplayPreferences;
window.saveTheme = saveTheme;
window.saveIconPack = saveIconPack;

View File

@@ -0,0 +1,125 @@
/**
* Linux BenchTools - Theme Manager
* Handles dynamic theme loading and switching
*/
(function() {
'use strict';
const THEME_STORAGE_KEY = 'benchtools_theme';
const DEFAULT_THEME = 'monokai-dark';
const THEMES = {
'monokai-dark': {
name: 'Monokai Dark',
file: 'css/themes/monokai-dark.css'
},
'monokai-light': {
name: 'Monokai Light',
file: 'css/themes/monokai-light.css'
},
'gruvbox-dark': {
name: 'Gruvbox Dark',
file: 'css/themes/gruvbox-dark.css'
},
'gruvbox-light': {
name: 'Gruvbox Light',
file: 'css/themes/gruvbox-light.css'
},
'mix-monokai-gruvbox': {
name: 'Mix Monokai-Gruvbox',
file: 'css/themes/mix-monokai-gruvbox.css'
}
};
/**
* Get the current theme from localStorage
* @returns {string} Theme identifier
*/
function getCurrentTheme() {
return localStorage.getItem(THEME_STORAGE_KEY) || DEFAULT_THEME;
}
/**
* Set the current theme in localStorage
* @param {string} theme - Theme identifier
*/
function setCurrentTheme(theme) {
if (!THEMES[theme]) {
console.warn(`Theme "${theme}" not found, using default`);
theme = DEFAULT_THEME;
}
localStorage.setItem(THEME_STORAGE_KEY, theme);
}
/**
* Load a theme CSS file
* @param {string} theme - Theme identifier
*/
function loadTheme(theme) {
if (!THEMES[theme]) {
console.warn(`Theme "${theme}" not found, using default`);
theme = DEFAULT_THEME;
}
// Remove existing theme link if present
const existingThemeLink = document.getElementById('theme-stylesheet');
if (existingThemeLink) {
existingThemeLink.remove();
}
// Create new theme link
const themeLink = document.createElement('link');
themeLink.id = 'theme-stylesheet';
themeLink.rel = 'stylesheet';
themeLink.href = THEMES[theme].file;
// Insert after the last stylesheet or in the head
const lastStylesheet = Array.from(document.head.querySelectorAll('link[rel="stylesheet"]')).pop();
if (lastStylesheet) {
lastStylesheet.after(themeLink);
} else {
document.head.appendChild(themeLink);
}
// Update body data attribute for theme-specific styling
document.body.setAttribute('data-theme', theme);
}
/**
* Apply theme and save preference
* @param {string} theme - Theme identifier
*/
function applyTheme(theme) {
setCurrentTheme(theme);
loadTheme(theme);
// Dispatch custom event for theme change
const event = new CustomEvent('themeChanged', {
detail: { theme, themeName: THEMES[theme].name }
});
window.dispatchEvent(event);
}
/**
* Initialize theme on page load
*/
function initTheme() {
const currentTheme = getCurrentTheme();
loadTheme(currentTheme);
}
// Initialize theme immediately
initTheme();
// Export API to window
window.ThemeManager = {
getCurrentTheme,
setCurrentTheme,
loadTheme,
applyTheme,
themes: THEMES,
defaultTheme: DEFAULT_THEME
};
})();