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>
This commit is contained in:
344
frontend/js/utils.js
Normal file
344
frontend/js/utils.js
Normal file
@@ -0,0 +1,344 @@
|
||||
// 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
|
||||
};
|
||||
Reference in New Issue
Block a user