Files
serv_benchmark/frontend/js/utils.js
gilles soulier c6a8e8e83d feat: Complete MVP implementation of Linux BenchTools
 Features:
- Backend FastAPI complete (25 Python files)
  - 5 SQLAlchemy models (Device, HardwareSnapshot, Benchmark, Link, Document)
  - Pydantic schemas for validation
  - 4 API routers (benchmark, devices, links, docs)
  - Authentication with Bearer token
  - Automatic score calculation
  - File upload support

- Frontend web interface (13 files)
  - 4 HTML pages (Dashboard, Devices, Device Detail, Settings)
  - 7 JavaScript modules
  - Monokai dark theme CSS
  - Responsive design
  - Complete CRUD operations

- Client benchmark script (500+ lines Bash)
  - Hardware auto-detection
  - CPU, RAM, Disk, Network benchmarks
  - JSON payload generation
  - Robust error handling

- Docker deployment
  - Optimized Dockerfile
  - docker-compose with 2 services
  - Persistent volumes
  - Environment variables

- Documentation & Installation
  - Automated install.sh script
  - README, QUICKSTART, DEPLOYMENT guides
  - Complete API documentation
  - Project structure documentation

📊 Stats:
- ~60 files created
- ~5000 lines of code
- Full MVP feature set implemented

🚀 Ready for production deployment!

🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-07 14:46:10 +01:00

345 lines
8.6 KiB
JavaScript

// 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];
}
// 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 '--';
return Math.round(score);
}
// 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 {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
console.error('Failed to copy:', err);
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
document.body.removeChild(textArea);
return true;
} catch (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');
}
}
// Initialize modal close buttons
document.addEventListener('DOMContentLoaded', () => {
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
window.BenchUtils = {
formatDate,
formatRelativeTime,
formatFileSize,
getScoreBadgeClass,
getScoreBadgeText,
createScoreBadge,
escapeHtml,
copyToClipboard,
showToast,
debounce,
parseTags,
formatTags,
getUrlParameter,
setUrlParameter,
showLoading,
hideLoading,
showError,
showEmptyState,
formatHardwareInfo,
initTabs,
openModal,
closeModal
};