549 lines
14 KiB
JavaScript
Executable File
549 lines
14 KiB
JavaScript
Executable File
// Linux BenchTools - Utility Functions
|
|
|
|
// Format date to readable string
|
|
function formatDate(dateString) {
|
|
if (!dateString) return 'N/A';
|
|
const date = new Date(dateString);
|
|
return date.toLocaleString('fr-FR', {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
}
|
|
|
|
// Format date to relative time
|
|
function formatRelativeTime(dateString) {
|
|
if (!dateString) return 'N/A';
|
|
const date = new Date(dateString);
|
|
const now = new Date();
|
|
const diff = now - date;
|
|
|
|
const seconds = Math.floor(diff / 1000);
|
|
const minutes = Math.floor(seconds / 60);
|
|
const hours = Math.floor(minutes / 60);
|
|
const days = Math.floor(hours / 24);
|
|
|
|
if (days > 0) return `il y a ${days} jour${days > 1 ? 's' : ''}`;
|
|
if (hours > 0) return `il y a ${hours} heure${hours > 1 ? 's' : ''}`;
|
|
if (minutes > 0) return `il y a ${minutes} minute${minutes > 1 ? 's' : ''}`;
|
|
return `il y a ${seconds} seconde${seconds > 1 ? 's' : ''}`;
|
|
}
|
|
|
|
// Format file size
|
|
function formatFileSize(bytes) {
|
|
if (!bytes || bytes === 0) return '0 B';
|
|
const k = 1024;
|
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
|
}
|
|
|
|
// Format duration (seconds) into human readable form
|
|
function formatDuration(seconds) {
|
|
if (seconds === null || seconds === undefined) return 'N/A';
|
|
const totalSeconds = Number(seconds);
|
|
if (!Number.isFinite(totalSeconds) || totalSeconds < 0) return 'N/A';
|
|
|
|
const days = Math.floor(totalSeconds / 86400);
|
|
const hours = Math.floor((totalSeconds % 86400) / 3600);
|
|
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
const secs = Math.floor(totalSeconds % 60);
|
|
|
|
const parts = [];
|
|
if (days > 0) parts.push(`${days} j`);
|
|
if (hours > 0) parts.push(`${hours} h`);
|
|
if (minutes > 0) parts.push(`${minutes} min`);
|
|
if (parts.length === 0) parts.push(`${secs} s`);
|
|
|
|
return parts.join(' ');
|
|
}
|
|
|
|
// Get score badge class based on value
|
|
function getScoreBadgeClass(score) {
|
|
if (score === null || score === undefined) return 'score-badge';
|
|
if (score >= 76) return 'score-badge score-high';
|
|
if (score >= 51) return 'score-badge score-medium';
|
|
return 'score-badge score-low';
|
|
}
|
|
|
|
// Get score badge text
|
|
function getScoreBadgeText(score) {
|
|
if (score === null || score === undefined) return '--';
|
|
const numeric = Number(score);
|
|
if (!Number.isFinite(numeric)) return '--';
|
|
return Math.ceil(numeric);
|
|
}
|
|
|
|
// Create score badge HTML
|
|
function createScoreBadge(score, label = '') {
|
|
const badgeClass = getScoreBadgeClass(score);
|
|
const scoreText = getScoreBadgeText(score);
|
|
const labelHtml = label ? `<div class="score-label">${label}</div>` : '';
|
|
|
|
return `
|
|
<div class="score-item">
|
|
${labelHtml}
|
|
<div class="${badgeClass}">${scoreText}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Escape HTML to prevent XSS
|
|
function escapeHtml(text) {
|
|
if (!text) return '';
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Copy text to clipboard
|
|
async function copyToClipboard(text) {
|
|
// Try modern clipboard API first
|
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
return true;
|
|
} catch (err) {
|
|
console.error('Clipboard API failed:', err);
|
|
// Fall through to fallback method
|
|
}
|
|
}
|
|
|
|
// Fallback for older browsers or non-HTTPS contexts
|
|
const textArea = document.createElement('textarea');
|
|
textArea.value = text;
|
|
textArea.style.position = 'fixed';
|
|
textArea.style.left = '-999999px';
|
|
textArea.style.top = '0';
|
|
document.body.appendChild(textArea);
|
|
textArea.focus();
|
|
textArea.select();
|
|
|
|
try {
|
|
const successful = document.execCommand('copy');
|
|
document.body.removeChild(textArea);
|
|
return successful;
|
|
} catch (err) {
|
|
console.error('Fallback copy failed:', err);
|
|
document.body.removeChild(textArea);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Show toast notification
|
|
function showToast(message, type = 'info') {
|
|
// Remove existing toasts
|
|
const existingToast = document.querySelector('.toast');
|
|
if (existingToast) {
|
|
existingToast.remove();
|
|
}
|
|
|
|
const toast = document.createElement('div');
|
|
toast.className = `toast toast-${type}`;
|
|
toast.style.cssText = `
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
padding: 1rem 1.5rem;
|
|
background-color: var(--bg-secondary);
|
|
border-left: 4px solid var(--color-${type === 'success' ? 'success' : type === 'error' ? 'danger' : 'info'});
|
|
border-radius: var(--radius-sm);
|
|
color: var(--text-primary);
|
|
z-index: 10000;
|
|
animation: slideIn 0.3s ease-out;
|
|
`;
|
|
toast.textContent = message;
|
|
|
|
document.body.appendChild(toast);
|
|
|
|
setTimeout(() => {
|
|
toast.style.animation = 'slideOut 0.3s ease-out';
|
|
setTimeout(() => toast.remove(), 300);
|
|
}, 3000);
|
|
}
|
|
|
|
// Add CSS animations for toast
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
@keyframes slideIn {
|
|
from {
|
|
transform: translateX(400px);
|
|
opacity: 0;
|
|
}
|
|
to {
|
|
transform: translateX(0);
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
@keyframes slideOut {
|
|
from {
|
|
transform: translateX(0);
|
|
opacity: 1;
|
|
}
|
|
to {
|
|
transform: translateX(400px);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
|
|
// Debounce function for search inputs
|
|
function debounce(func, wait) {
|
|
let timeout;
|
|
return function executedFunction(...args) {
|
|
const later = () => {
|
|
clearTimeout(timeout);
|
|
func(...args);
|
|
};
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
};
|
|
}
|
|
|
|
// Parse tags from string
|
|
function parseTags(tagsString) {
|
|
if (!tagsString) return [];
|
|
if (Array.isArray(tagsString)) return tagsString;
|
|
|
|
try {
|
|
// Try to parse as JSON
|
|
return JSON.parse(tagsString);
|
|
} catch {
|
|
// Fall back to comma-separated
|
|
return tagsString.split(',').map(tag => tag.trim()).filter(tag => tag);
|
|
}
|
|
}
|
|
|
|
// Format tags as HTML
|
|
function formatTags(tagsString) {
|
|
const tags = parseTags(tagsString);
|
|
if (tags.length === 0) return '<span class="text-muted">Aucun tag</span>';
|
|
|
|
return tags.map(tag =>
|
|
`<span class="tag tag-primary">${escapeHtml(tag)}</span>`
|
|
).join('');
|
|
}
|
|
|
|
// Get URL parameter
|
|
function getUrlParameter(name) {
|
|
const params = new URLSearchParams(window.location.search);
|
|
return params.get(name);
|
|
}
|
|
|
|
// Set URL parameter without reload
|
|
function setUrlParameter(name, value) {
|
|
const url = new URL(window.location);
|
|
url.searchParams.set(name, value);
|
|
window.history.pushState({}, '', url);
|
|
}
|
|
|
|
// Loading state management
|
|
function showLoading(element) {
|
|
if (!element) return;
|
|
element.innerHTML = '<div class="loading">Chargement</div>';
|
|
}
|
|
|
|
function hideLoading(element) {
|
|
if (!element) return;
|
|
const loading = element.querySelector('.loading');
|
|
if (loading) loading.remove();
|
|
}
|
|
|
|
// Error display
|
|
function showError(element, message) {
|
|
if (!element) return;
|
|
element.innerHTML = `
|
|
<div class="error">
|
|
<strong>Erreur:</strong> ${escapeHtml(message)}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Empty state display
|
|
function showEmptyState(element, message, icon = '📭') {
|
|
if (!element) return;
|
|
element.innerHTML = `
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">${icon}</div>
|
|
<p>${escapeHtml(message)}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Format hardware info for display
|
|
function formatHardwareInfo(snapshot) {
|
|
if (!snapshot) return {};
|
|
|
|
return {
|
|
cpu: `${snapshot.cpu_model || 'N/A'} (${snapshot.cpu_cores || 0}C/${snapshot.cpu_threads || 0}T)`,
|
|
ram: `${Math.round((snapshot.ram_total_mb || 0) / 1024)} GB`,
|
|
gpu: snapshot.gpu_summary || snapshot.gpu_model || 'N/A',
|
|
storage: snapshot.storage_summary || 'N/A',
|
|
os: `${snapshot.os_name || 'N/A'} ${snapshot.os_version || ''}`,
|
|
kernel: snapshot.kernel_version || 'N/A'
|
|
};
|
|
}
|
|
|
|
// Tab management
|
|
function initTabs(containerSelector) {
|
|
const container = document.querySelector(containerSelector);
|
|
if (!container) return;
|
|
|
|
const tabs = container.querySelectorAll('.tab');
|
|
const contents = container.querySelectorAll('.tab-content');
|
|
|
|
tabs.forEach(tab => {
|
|
tab.addEventListener('click', () => {
|
|
// Remove active class from all tabs and contents
|
|
tabs.forEach(t => t.classList.remove('active'));
|
|
contents.forEach(c => c.classList.remove('active'));
|
|
|
|
// Add active class to clicked tab
|
|
tab.classList.add('active');
|
|
|
|
// Show corresponding content
|
|
const targetId = tab.dataset.tab;
|
|
const targetContent = container.querySelector(`#${targetId}`);
|
|
if (targetContent) {
|
|
targetContent.classList.add('active');
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Modal management
|
|
function openModal(modalId) {
|
|
const modal = document.getElementById(modalId);
|
|
if (modal) {
|
|
modal.classList.add('active');
|
|
}
|
|
}
|
|
|
|
function closeModal(modalId) {
|
|
const modal = document.getElementById(modalId);
|
|
if (modal) {
|
|
modal.classList.remove('active');
|
|
}
|
|
}
|
|
|
|
// Apply icon size preferences from localStorage
|
|
function applyIconSizePreferences() {
|
|
const sectionIconSize = localStorage.getItem('displayPref_sectionIconSize') || '32';
|
|
const buttonIconSize = localStorage.getItem('displayPref_buttonIconSize') || '24';
|
|
|
|
document.documentElement.style.setProperty('--section-icon-size', `${sectionIconSize}px`);
|
|
document.documentElement.style.setProperty('--button-icon-size', `${buttonIconSize}px`);
|
|
}
|
|
|
|
// Initialize modal close buttons and apply icon preferences
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// Apply icon size preferences on all pages
|
|
applyIconSizePreferences();
|
|
|
|
// Initialize modal close buttons
|
|
document.querySelectorAll('.modal').forEach(modal => {
|
|
modal.addEventListener('click', (e) => {
|
|
if (e.target === modal) {
|
|
modal.classList.remove('active');
|
|
}
|
|
});
|
|
|
|
const closeBtn = modal.querySelector('.modal-close');
|
|
if (closeBtn) {
|
|
closeBtn.addEventListener('click', () => {
|
|
modal.classList.remove('active');
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
// Export functions for use in other files
|
|
// Unit conversion functions
|
|
function getDisplayPreferences() {
|
|
return {
|
|
memoryUnit: localStorage.getItem('displayPref_memoryUnit') || 'GB',
|
|
storageUnit: localStorage.getItem('displayPref_storageUnit') || 'GB',
|
|
cacheUnit: localStorage.getItem('displayPref_cacheUnit') || 'KB',
|
|
temperatureUnit: localStorage.getItem('displayPref_temperatureUnit') || 'C'
|
|
};
|
|
}
|
|
|
|
// Convert memory from MB to preferred unit
|
|
function formatMemory(mb, forceUnit = null) {
|
|
if (!mb || mb === 0) return '0 MB';
|
|
|
|
const prefs = getDisplayPreferences();
|
|
const unit = forceUnit || prefs.memoryUnit;
|
|
|
|
if (unit === 'GB') {
|
|
return (mb / 1024).toFixed(2) + ' GB';
|
|
}
|
|
return mb.toFixed(0) + ' MB';
|
|
}
|
|
|
|
// Convert storage from GB to preferred unit
|
|
function formatStorage(gb, forceUnit = null) {
|
|
if (!gb || gb === 0) return '0 GB';
|
|
|
|
const prefs = getDisplayPreferences();
|
|
const unit = forceUnit || prefs.storageUnit;
|
|
|
|
if (unit === 'TB') {
|
|
return (gb / 1024).toFixed(2) + ' TB';
|
|
} else if (unit === 'MB') {
|
|
return (gb * 1024).toFixed(0) + ' MB';
|
|
}
|
|
return gb.toFixed(2) + ' GB';
|
|
}
|
|
|
|
// Convert cache from KB to preferred unit
|
|
function formatCache(kb, forceUnit = null) {
|
|
if (!kb || kb === 0) return '0 KB';
|
|
|
|
const prefs = getDisplayPreferences();
|
|
const unit = forceUnit || prefs.cacheUnit;
|
|
|
|
if (unit === 'MB') {
|
|
return (kb / 1024).toFixed(2) + ' MB';
|
|
}
|
|
return kb.toFixed(0) + ' KB';
|
|
}
|
|
|
|
// Convert temperature to preferred unit
|
|
function formatTemperature(celsius, forceUnit = null) {
|
|
if (celsius === null || celsius === undefined) return 'N/A';
|
|
|
|
const prefs = getDisplayPreferences();
|
|
const unit = forceUnit || prefs.temperatureUnit;
|
|
|
|
if (unit === 'F') {
|
|
const fahrenheit = (celsius * 9/5) + 32;
|
|
return fahrenheit.toFixed(1) + '°F';
|
|
}
|
|
return celsius.toFixed(1) + '°C';
|
|
}
|
|
|
|
function renderMarkdown(text) {
|
|
if (!text || !text.trim()) {
|
|
return '<div class="markdown-block" style="color: var(--text-secondary);">Aucune note disponible</div>';
|
|
}
|
|
|
|
let html = escapeHtml(text);
|
|
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
|
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
html = html.replace(/\n/g, '<br>');
|
|
|
|
return `<div class="markdown-block">${html}</div>`;
|
|
}
|
|
|
|
// Additional utility functions for peripherals module
|
|
function formatDateTime(dateString) {
|
|
if (!dateString) return 'N/A';
|
|
const date = new Date(dateString);
|
|
return date.toLocaleString('fr-FR', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
}
|
|
|
|
function formatBytes(bytes) {
|
|
return formatFileSize(bytes);
|
|
}
|
|
|
|
function showSuccess(message) {
|
|
showToast(message, 'success');
|
|
}
|
|
|
|
function showInfo(message) {
|
|
showToast(message, 'info');
|
|
}
|
|
|
|
// API request helper
|
|
async function apiRequest(endpoint, options = {}) {
|
|
const config = window.CONFIG || { API_BASE_URL: 'http://localhost:8007/api' };
|
|
const url = `${config.API_BASE_URL}${endpoint}`;
|
|
|
|
const defaultOptions = {
|
|
headers: {}
|
|
};
|
|
|
|
// Don't set Content-Type for FormData (browser will set it with boundary)
|
|
if (options.body && !(options.body instanceof FormData)) {
|
|
defaultOptions.headers['Content-Type'] = 'application/json';
|
|
}
|
|
|
|
const response = await fetch(url, {
|
|
...defaultOptions,
|
|
...options,
|
|
headers: {
|
|
...defaultOptions.headers,
|
|
...options.headers
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
let message = `HTTP ${response.status}: ${response.statusText}`;
|
|
try {
|
|
const data = await response.json();
|
|
if (data && typeof data === 'object') {
|
|
message = data.detail || data.message || message;
|
|
}
|
|
} catch (error) {
|
|
// Ignore JSON parse errors and keep default message.
|
|
}
|
|
throw new Error(message);
|
|
}
|
|
|
|
// Handle 204 No Content
|
|
if (response.status === 204) {
|
|
return null;
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
window.BenchUtils = {
|
|
formatDate,
|
|
formatRelativeTime,
|
|
formatFileSize,
|
|
formatDuration,
|
|
getScoreBadgeClass,
|
|
getScoreBadgeText,
|
|
createScoreBadge,
|
|
escapeHtml,
|
|
copyToClipboard,
|
|
showToast,
|
|
debounce,
|
|
parseTags,
|
|
formatTags,
|
|
getUrlParameter,
|
|
setUrlParameter,
|
|
showLoading,
|
|
hideLoading,
|
|
showError,
|
|
showEmptyState,
|
|
formatHardwareInfo,
|
|
initTabs,
|
|
openModal,
|
|
closeModal,
|
|
getDisplayPreferences,
|
|
formatMemory,
|
|
formatStorage,
|
|
formatCache,
|
|
formatTemperature,
|
|
renderMarkdown,
|
|
formatDateTime,
|
|
formatBytes,
|
|
showSuccess,
|
|
showInfo,
|
|
apiRequest
|
|
};
|