This commit is contained in:
2025-12-20 03:47:10 +01:00
parent 8428bf9c82
commit dcba044cd6
179 changed files with 10345 additions and 786 deletions

11
frontend/js/api.js Normal file → Executable file
View File

@@ -1,6 +1,7 @@
// Linux BenchTools - API Client
const API_BASE_URL = window.location.protocol + '//' + window.location.hostname + ':8007/api';
const API_BASE_URL = (window.BenchConfig && window.BenchConfig.backendApiUrl)
|| `${window.location.protocol}//${window.location.hostname}:8007/api`;
class BenchAPI {
constructor(baseURL = API_BASE_URL) {
@@ -143,6 +144,14 @@ class BenchAPI {
return this.get(`/benchmarks/${benchmarkId}`);
}
// Update benchmark fields
async updateBenchmark(benchmarkId, data) {
return this.request(`/benchmarks/${benchmarkId}`, {
method: 'PATCH',
body: JSON.stringify(data)
});
}
// Get all benchmarks
async getAllBenchmarks(params = {}) {
return this.get('/benchmarks', params);

45
frontend/js/dashboard.js Normal file → Executable file
View File

@@ -6,6 +6,23 @@ const api = window.BenchAPI;
// Global state
let allDevices = [];
let isLoading = false;
let apiToken = null;
let iperfServer = null;
// Load backend config (API token, etc.)
async function loadBackendConfig() {
try {
const response = await fetch(`${window.BenchConfig.backendApiUrl}/config`);
if (response.ok) {
const config = await response.json();
apiToken = config.api_token;
iperfServer = config.iperf_server || '10.0.1.97';
updateBenchCommandDisplay();
}
} catch (error) {
console.error('Failed to load backend config:', error);
}
}
// Load dashboard data
async function loadDashboard() {
@@ -83,7 +100,7 @@ async function loadStats() {
}
});
const avgScore = scoreCount > 0 ? Math.round(scoreSum / scoreCount) : 0;
const avgScore = scoreCount > 0 ? Math.ceil(scoreSum / scoreCount) : 0;
// Update UI
document.getElementById('totalDevices').textContent = totalDevices;
@@ -221,6 +238,26 @@ function createDeviceRow(device, rank) {
`;
}
function buildBenchCommand() {
const cfg = window.BenchConfig || {};
const frontendBase = (cfg.frontendBaseUrl || window.location.origin).replace(/\/$/, '');
const scriptPath = cfg.benchScriptPath || '/scripts/bench.sh';
const backendBase = (cfg.backendApiUrl || `${window.location.protocol}//${window.location.hostname}:8007/api`).replace(/\/$/, '');
const token = apiToken || 'LOADING...';
const iperf = iperfServer || '10.0.1.97';
// Extract backend URL without /api suffix
const backendUrl = backendBase.replace(/\/api$/, '');
return `curl -fsSL ${frontendBase}${scriptPath} | sudo bash -s -- --server ${backendUrl} --token "${token}" --iperf-server ${iperf}`;
}
function updateBenchCommandDisplay() {
const element = document.getElementById('benchCommand');
if (!element) return;
element.textContent = buildBenchCommand();
}
// Copy bench command to clipboard
async function copyBenchCommand() {
const command = document.getElementById('benchCommand').textContent;
@@ -282,7 +319,11 @@ function refreshDashboard() {
}
// Initialize dashboard on page load
document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('DOMContentLoaded', async () => {
// Load backend config first to get API token
await loadBackendConfig();
// Then load dashboard data
loadDashboard();
// Setup search input listener

152
frontend/js/device_detail.js Normal file → Executable file
View File

@@ -1,6 +1,6 @@
// Linux BenchTools - Device Detail Logic
const { formatDate, formatRelativeTime, formatFileSize, createScoreBadge, getScoreBadgeText, escapeHtml, showError, showEmptyState, formatTags, initTabs, openModal, showToast, formatHardwareInfo } = window.BenchUtils;
const { formatDate, formatRelativeTime, formatFileSize, createScoreBadge, getScoreBadgeText, escapeHtml, showError, showEmptyState, formatTags, initTabs, openModal, showToast, formatHardwareInfo, formatDuration, formatStorage } = window.BenchUtils;
const api = window.BenchAPI;
let currentDeviceId = null;
@@ -46,6 +46,11 @@ async function loadDeviceDetail() {
await loadDocuments();
await loadLinks();
const deleteBtn = document.getElementById('deleteDeviceBtn');
if (deleteBtn) {
deleteBtn.addEventListener('click', handleDeleteDevice);
}
} catch (error) {
console.error('Failed to load device:', error);
document.getElementById('loadingState').innerHTML =
@@ -300,7 +305,7 @@ function renderStorageDetails() {
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 0.75rem; font-size: 0.9rem;">
${disk.capacity_gb ? `
<div>
<strong>Capacité:</strong> ${disk.capacity_gb} GB
<strong>Capacité:</strong> ${formatStorage(disk.capacity_gb)}
</div>
` : ''}
${disk.type ? `
@@ -331,7 +336,114 @@ function renderStorageDetails() {
console.error('Failed to parse storage devices:', e);
html = '<p style="color: var(--text-danger);">Erreur lors du parsing des données de stockage</p>';
}
} else {
}
// Parse partitions
if (snapshot.partitions_json) {
try {
const partitions = typeof snapshot.partitions_json === 'string'
? JSON.parse(snapshot.partitions_json)
: snapshot.partitions_json;
if (Array.isArray(partitions) && partitions.length > 0) {
html += `
<div style="margin-top: 1.5rem;">
<h4 style="margin-bottom: 0.75rem; color: var(--text-secondary);">Partitions et volumes</h4>
<div style="overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="text-align: left; border-bottom: 1px solid var(--border-color); color: var(--text-secondary);">
<th style="padding: 0.5rem;">Partition</th>
<th style="padding: 0.5rem;">Montage</th>
<th style="padding: 0.5rem;">Type</th>
<th style="padding: 0.5rem;">Utilisé</th>
<th style="padding: 0.5rem;">Libre</th>
<th style="padding: 0.5rem;">Total</th>
</tr>
</thead>
<tbody>
${partitions.map(part => {
const used = typeof part.used_gb === 'number' ? formatStorage(part.used_gb) : 'N/A';
const free = typeof part.free_gb === 'number'
? formatStorage(part.free_gb)
: (typeof part.total_gb === 'number' && typeof part.used_gb === 'number'
? formatStorage(part.total_gb - part.used_gb)
: 'N/A');
const total = typeof part.total_gb === 'number' ? formatStorage(part.total_gb) : 'N/A';
return `
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 0.5rem; font-weight: 600;">${escapeHtml(part.name || 'N/A')}</td>
<td style="padding: 0.5rem;">${part.mount_point ? escapeHtml(part.mount_point) : '<span style="color: var(--text-muted);">Non monté</span>'}</td>
<td style="padding: 0.5rem;">${part.fs_type ? escapeHtml(part.fs_type) : 'N/A'}</td>
<td style="padding: 0.5rem;">${used}</td>
<td style="padding: 0.5rem;">${free}</td>
<td style="padding: 0.5rem;">${total}</td>
</tr>
`;
}).join('')}
</tbody>
</table>
</div>
</div>
`;
}
} catch (error) {
console.error('Failed to parse partitions:', error);
html += '<p style="color: var(--color-danger); margin-top: 1rem;">Erreur lors de la lecture des partitions</p>';
}
}
if (snapshot.network_shares_json) {
try {
const shares = typeof snapshot.network_shares_json === 'string'
? JSON.parse(snapshot.network_shares_json)
: snapshot.network_shares_json;
if (Array.isArray(shares) && shares.length > 0) {
html += `
<div style="margin-top: 1.5rem;">
<h4 style="margin-bottom: 0.75rem; color: var(--text-secondary);">Partages réseau montés</h4>
<div style="overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="text-align: left; border-bottom: 1px solid var(--border-color); color: var(--text-secondary);">
<th style="padding: 0.5rem;">Source</th>
<th style="padding: 0.5rem;">Montage</th>
<th style="padding: 0.5rem;">Protocole</th>
<th style="padding: 0.5rem;">Type</th>
<th style="padding: 0.5rem;">Utilisé</th>
<th style="padding: 0.5rem;">Libre</th>
<th style="padding: 0.5rem;">Total</th>
<th style="padding: 0.5rem;">Options</th>
</tr>
</thead>
<tbody>
${shares.map(share => `
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 0.5rem;">${escapeHtml(share.source || 'N/A')}</td>
<td style="padding: 0.5rem;">${escapeHtml(share.mount_point || 'N/A')}</td>
<td style="padding: 0.5rem;">${escapeHtml(share.protocol || share.fs_type || 'N/A')}</td>
<td style="padding: 0.5rem;">${share.fs_type ? escapeHtml(share.fs_type) : 'N/A'}</td>
<td style="padding: 0.5rem;">${typeof share.used_gb === 'number' ? formatStorage(share.used_gb) : 'N/A'}</td>
<td style="padding: 0.5rem;">${typeof share.free_gb === 'number' ? formatStorage(share.free_gb) : 'N/A'}</td>
<td style="padding: 0.5rem;">${typeof share.total_gb === 'number' ? formatStorage(share.total_gb) : 'N/A'}</td>
<td style="padding: 0.5rem; max-width: 200px;">${share.options ? escapeHtml(share.options) : '<span style="color: var(--text-muted);">N/A</span>'}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
`;
}
} catch (error) {
console.error('Failed to parse network shares:', error);
html += '<p style="color: var(--color-danger); margin-top: 1rem;">Erreur lors de la lecture des partages réseau</p>';
}
}
if (!html) {
html = '<p style="color: var(--text-muted);">Aucune information de stockage disponible</p>';
}
@@ -407,11 +519,25 @@ function renderOSDetails() {
return;
}
const displayServer = snapshot.display_server || snapshot.session_type || 'N/A';
const resolution = snapshot.screen_resolution || 'N/A';
const lastBoot = snapshot.last_boot_time || 'N/A';
const uptime = snapshot.uptime_seconds != null ? formatDuration(snapshot.uptime_seconds) : 'N/A';
const battery = snapshot.battery_percentage != null
? `${snapshot.battery_percentage}%${snapshot.battery_status ? ` (${snapshot.battery_status})` : ''}`
: 'N/A';
const items = [
{ label: 'Nom', 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: 'Environnement', value: snapshot.desktop_environment || 'N/A' },
{ label: 'Session', value: displayServer },
{ label: 'Résolution écran', value: resolution },
{ label: 'Dernier boot', value: lastBoot },
{ label: 'Uptime', value: uptime },
{ label: 'Batterie', value: battery },
{ label: 'Virtualisation', value: snapshot.virtualization_type || 'none' }
];
@@ -427,6 +553,26 @@ function renderOSDetails() {
`;
}
async function handleDeleteDevice() {
if (!currentDevice) return;
const confirmed = confirm(`Voulez-vous vraiment supprimer le device "${currentDevice.hostname}" ? Cette action est définitive.`);
if (!confirmed) {
return;
}
try {
await api.deleteDevice(currentDevice.id);
showToast('Device supprimé avec succès', 'success');
setTimeout(() => {
window.location.href = 'devices.html';
}, 800);
} catch (error) {
console.error('Failed to delete device:', error);
showToast(error.message || 'Impossible de supprimer le device', 'error');
}
}
// Render Benchmark Results
function renderBenchmarkResults() {
const bench = currentDevice.last_benchmark;

1812
frontend/js/devices.js Normal file → Executable file

File diff suppressed because it is too large Load Diff

104
frontend/js/settings.js Normal file → Executable file
View File

@@ -3,14 +3,103 @@
const { copyToClipboard, showToast, escapeHtml } = window.BenchUtils;
let tokenVisible = false;
const API_TOKEN = 'YOUR_API_TOKEN_HERE'; // Will be replaced by actual token or fetched from backend
let API_TOKEN = 'LOADING...';
// Load backend config (API token, etc.)
async function loadBackendConfig() {
try {
const backendApiUrl = window.BenchConfig?.backendApiUrl || `${window.location.protocol}//${window.location.hostname}:8007/api`;
const response = await fetch(`${backendApiUrl}/config`);
if (response.ok) {
const config = await response.json();
API_TOKEN = config.api_token;
document.getElementById('apiToken').value = API_TOKEN;
generateBenchCommand();
}
} catch (error) {
console.error('Failed to load backend config:', error);
API_TOKEN = 'ERROR_LOADING_TOKEN';
}
}
// Initialize settings page
document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('DOMContentLoaded', async () => {
loadDisplayPreferences();
loadSettings();
generateBenchCommand();
await loadBackendConfig();
});
// Load display preferences
function loadDisplayPreferences() {
const memoryUnit = localStorage.getItem('displayPref_memoryUnit') || 'GB';
const storageUnit = localStorage.getItem('displayPref_storageUnit') || 'GB';
const cacheUnit = localStorage.getItem('displayPref_cacheUnit') || 'KB';
const temperatureUnit = localStorage.getItem('displayPref_temperatureUnit') || 'C';
const sectionIconSize = localStorage.getItem('displayPref_sectionIconSize') || '32';
const buttonIconSize = localStorage.getItem('displayPref_buttonIconSize') || '24';
document.getElementById('memoryUnit').value = memoryUnit;
document.getElementById('storageUnit').value = storageUnit;
document.getElementById('cacheUnit').value = cacheUnit;
document.getElementById('temperatureUnit').value = temperatureUnit;
document.getElementById('sectionIconSize').value = sectionIconSize;
document.getElementById('buttonIconSize').value = buttonIconSize;
// Apply icon sizes
applyIconSizes(sectionIconSize, buttonIconSize);
}
// Apply icon sizes dynamically
function applyIconSizes(sectionIconSize, buttonIconSize) {
document.documentElement.style.setProperty('--section-icon-size', `${sectionIconSize}px`);
document.documentElement.style.setProperty('--button-icon-size', `${buttonIconSize}px`);
}
// Save display preferences
function saveDisplayPreferences() {
const memoryUnit = document.getElementById('memoryUnit').value;
const storageUnit = document.getElementById('storageUnit').value;
const cacheUnit = document.getElementById('cacheUnit').value;
const temperatureUnit = document.getElementById('temperatureUnit').value;
const sectionIconSize = document.getElementById('sectionIconSize').value;
const buttonIconSize = document.getElementById('buttonIconSize').value;
localStorage.setItem('displayPref_memoryUnit', memoryUnit);
localStorage.setItem('displayPref_storageUnit', storageUnit);
localStorage.setItem('displayPref_cacheUnit', cacheUnit);
localStorage.setItem('displayPref_temperatureUnit', temperatureUnit);
localStorage.setItem('displayPref_sectionIconSize', sectionIconSize);
localStorage.setItem('displayPref_buttonIconSize', buttonIconSize);
// Apply icon sizes immediately
applyIconSizes(sectionIconSize, buttonIconSize);
showToast('Préférences enregistrées ! Rechargez la page pour voir les changements.', 'success');
}
// Reset display preferences to defaults
function resetDisplayPreferences() {
localStorage.removeItem('displayPref_memoryUnit');
localStorage.removeItem('displayPref_storageUnit');
localStorage.removeItem('displayPref_cacheUnit');
localStorage.removeItem('displayPref_temperatureUnit');
localStorage.removeItem('displayPref_sectionIconSize');
localStorage.removeItem('displayPref_buttonIconSize');
// Reset form to defaults
document.getElementById('memoryUnit').value = 'GB';
document.getElementById('storageUnit').value = 'GB';
document.getElementById('cacheUnit').value = 'KB';
document.getElementById('temperatureUnit').value = 'C';
document.getElementById('sectionIconSize').value = '32';
document.getElementById('buttonIconSize').value = '24';
// Apply default icon sizes
applyIconSizes('32', '24');
showToast('Préférences réinitialisées aux valeurs par défaut', 'success');
}
// Load settings
function loadSettings() {
// In a real scenario, these would be fetched from backend or localStorage
@@ -22,8 +111,7 @@ function loadSettings() {
document.getElementById('iperfServer').value = savedIperfServer;
document.getElementById('benchMode').value = savedBenchMode;
// Set API token (in production, this should be fetched securely)
document.getElementById('apiToken').value = API_TOKEN;
// API token will be loaded asynchronously from backend
// Add event listeners for auto-generation
document.getElementById('backendUrl').addEventListener('input', () => {
@@ -74,8 +162,8 @@ function generateBenchCommand() {
const scriptUrl = `${backendUrl.replace(':8007', ':8087')}/scripts/bench.sh`;
// Build command parts
let command = `curl -s ${scriptUrl} | bash -s -- \\
--server ${backendUrl}/api/benchmark \\
let command = `curl -s ${scriptUrl} | sudo bash -s -- \\
--server ${backendUrl} \\
--token "${API_TOKEN}"`;
if (iperfServer) {
@@ -143,3 +231,5 @@ window.generateBenchCommand = generateBenchCommand;
window.copyGeneratedCommand = copyGeneratedCommand;
window.toggleTokenVisibility = toggleTokenVisibility;
window.copyToken = copyToken;
window.saveDisplayPreferences = saveDisplayPreferences;
window.resetDisplayPreferences = resetDisplayPreferences;

127
frontend/js/utils.js Normal file → Executable file
View File

@@ -40,6 +40,26 @@ function formatFileSize(bytes) {
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';
@@ -51,7 +71,9 @@ function getScoreBadgeClass(score) {
// Get score badge text
function getScoreBadgeText(score) {
if (score === null || score === undefined) return '--';
return Math.round(score);
const numeric = Number(score);
if (!Number.isFinite(numeric)) return '--';
return Math.ceil(numeric);
}
// Create score badge HTML
@@ -299,8 +321,21 @@ function closeModal(modalId) {
}
}
// Initialize modal close buttons
// 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) {
@@ -318,10 +353,90 @@ document.addEventListener('DOMContentLoaded', () => {
});
// 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>`;
}
window.BenchUtils = {
formatDate,
formatRelativeTime,
formatFileSize,
formatDuration,
getScoreBadgeClass,
getScoreBadgeText,
createScoreBadge,
@@ -340,5 +455,11 @@ window.BenchUtils = {
formatHardwareInfo,
initTabs,
openModal,
closeModal
closeModal,
getDisplayPreferences,
formatMemory,
formatStorage,
formatCache,
formatTemperature,
renderMarkdown
};