Files
serv_benchmark/frontend/js/device_detail.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

407 lines
14 KiB
JavaScript

// Linux BenchTools - Device Detail Logic
const { formatDate, formatRelativeTime, formatFileSize, createScoreBadge, getScoreBadgeText, escapeHtml, showError, showEmptyState, formatTags, initTabs, openModal, showToast, formatHardwareInfo } = 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();
renderHardwareSummary();
renderLastBenchmark();
await loadBenchmarkHistory();
await loadDocuments();
await loadLinks();
} 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 hardware summary
function renderHardwareSummary() {
const snapshot = currentDevice.last_hardware_snapshot;
if (!snapshot) {
document.getElementById('hardwareSummary').innerHTML =
'<p style="color: var(--text-muted);">Aucune information hardware disponible</p>';
return;
}
const hardwareItems = [
{ label: 'CPU', icon: '🔲', value: `${snapshot.cpu_model || 'N/A'}<br><small>${snapshot.cpu_cores || 0}C / ${snapshot.cpu_threads || 0}T @ ${snapshot.cpu_max_freq_ghz || snapshot.cpu_base_freq_ghz || '?'} GHz</small>` },
{ label: 'RAM', icon: '💾', value: `${Math.round((snapshot.ram_total_mb || 0) / 1024)} GB<br><small>${snapshot.ram_slots_used || '?'} / ${snapshot.ram_slots_total || '?'} slots</small>` },
{ label: 'GPU', icon: '🎮', value: snapshot.gpu_model || snapshot.gpu_summary || 'N/A' },
{ label: 'Stockage', icon: '💿', value: snapshot.storage_summary || 'N/A' },
{ label: 'Réseau', icon: '🌐', value: snapshot.network_interfaces_json ? `${JSON.parse(snapshot.network_interfaces_json).length} interface(s)` : 'N/A' },
{ label: 'Carte mère', icon: '⚡', value: `${snapshot.motherboard_vendor || ''} ${snapshot.motherboard_model || 'N/A'}` },
{ label: 'OS', icon: '🐧', value: `${snapshot.os_name || 'N/A'} ${snapshot.os_version || ''}<br><small>Kernel ${snapshot.kernel_version || 'N/A'}</small>` },
{ label: 'Architecture', icon: '🏗️', value: snapshot.architecture || 'N/A' },
{ label: 'Virtualisation', icon: '📦', value: snapshot.virtualization_type || 'none' }
];
document.getElementById('hardwareSummary').innerHTML = hardwareItems.map(item => `
<div class="hardware-item">
<div class="hardware-item-label">${item.icon} ${item.label}</div>
<div class="hardware-item-value">${item.value}</div>
</div>
`).join('');
}
// Render last benchmark scores
function renderLastBenchmark() {
const bench = currentDevice.last_benchmark;
if (!bench) {
document.getElementById('lastBenchmark').innerHTML =
'<p style="color: var(--text-muted);">Aucun benchmark disponible</p>';
return;
}
document.getElementById('lastBenchmark').innerHTML = `
<div style="margin-bottom: 1rem;">
<span style="color: var(--text-secondary);">Date: </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, '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: 1rem;">
<button class="btn btn-secondary btn-sm" onclick="viewBenchmarkDetails(${bench.id})">
Voir les détails complets (JSON)
</button>
</div>
`;
}
// 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 {
const docs = await api.getDeviceDocs(currentDeviceId);
if (!docs || docs.length === 0) {
showEmptyState(container, 'Aucun document uploadé', '📄');
return;
}
container.innerHTML = `
<ul class="document-list">
${docs.map(doc => `
<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>
`).join('')}
</ul>
`;
} 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 = {
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 documents
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');
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;