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

28
frontend/config.js Executable file
View File

@@ -0,0 +1,28 @@
// Frontend configuration (can be overridden by defining window.BenchConfig before loading this file)
(function() {
window.BenchConfig = window.BenchConfig || {};
const origin = window.location.origin;
const protocol = window.location.protocol;
const hostname = window.location.hostname;
if (!window.BenchConfig.frontendBaseUrl) {
window.BenchConfig.frontendBaseUrl = origin;
}
if (!window.BenchConfig.backendApiUrl) {
window.BenchConfig.backendApiUrl = `${protocol}//${hostname}:8007/api`;
}
if (!window.BenchConfig.benchScriptPath) {
window.BenchConfig.benchScriptPath = '/scripts/bench.sh';
}
if (!window.BenchConfig.apiTokenPlaceholder) {
window.BenchConfig.apiTokenPlaceholder = 'YOUR_TOKEN';
}
if (!window.BenchConfig.iperfServer) {
window.BenchConfig.iperfServer = '10.0.1.97';
}
})();

13
frontend/css/components.css Normal file → Executable file
View File

@@ -475,6 +475,9 @@
color: var(--text-secondary);
font-size: 0.75rem;
border: 1px solid var(--bg-tertiary);
display: inline-flex;
align-items: center;
gap: 0.25rem;
}
.tag-primary {
@@ -483,6 +486,16 @@
border-color: var(--color-info);
}
.tag .remove-tag {
background: transparent;
border: none;
color: inherit;
cursor: pointer;
font-size: 0.8rem;
padding: 0;
line-height: 1;
}
/* Alert Component */
.alert {
padding: var(--spacing-md);

265
frontend/css/main.css Normal file → Executable file
View File

@@ -16,6 +16,7 @@
--color-info: #66d9ef;
--color-purple: #ae81ff;
--color-yellow: #e6db74;
--border-color: #444444;
/* Spacing */
--spacing-xs: 0.25rem;
@@ -28,6 +29,12 @@
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
/* Icon sizing (customisable) */
--section-icon-size: 32px;
--button-icon-size: 24px;
--icon-btn-size: 42px;
--icon-btn-icon-size: 26px;
}
/* Reset & Base */
@@ -198,6 +205,264 @@ td {
color: var(--text-primary);
}
/* Device sections */
.device-section {
background: var(--bg-secondary);
border: 1px solid var(--bg-tertiary);
border-radius: var(--radius-md);
padding: var(--spacing-md);
margin-bottom: var(--spacing-md);
}
.device-section .section-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--spacing-md);
margin-bottom: var(--spacing-sm);
border-bottom: 1px solid var(--bg-tertiary);
padding-bottom: var(--spacing-sm);
}
.device-section .section-header h3 {
margin: 0;
font-size: 1.05rem;
color: var(--color-info);
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.section-icon-wrap {
display: inline-flex;
align-items: center;
justify-content: center;
}
.section-icon {
width: var(--section-icon-size);
height: var(--section-icon-size);
object-fit: contain;
filter: drop-shadow(0 0 2px rgba(0,0,0,0.4));
}
.section-title {
line-height: 1.2;
}
.section-actions {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.icon-btn {
background: var(--bg-primary);
border: 1px solid var(--bg-tertiary);
border-radius: 50%;
width: 38px;
height: 38px;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
padding: 0;
transition: transform 0.2s ease, border-color 0.2s ease;
text-decoration: none;
color: var(--text-primary);
}
.icon-btn img {
width: var(--button-icon-size);
height: var(--button-icon-size);
object-fit: contain;
}
.icon-btn:hover {
transform: scale(1.05);
border-color: var(--color-info);
}
.icon-btn.danger {
border-color: var(--color-danger);
}
.icon-btn.success {
border-color: var(--color-success);
}
.doc-actions {
display: flex;
gap: var(--spacing-xs);
}
.doc-actions .icon-btn {
width: 32px;
height: 32px;
}
.device-preamble {
border: 1px solid var(--bg-tertiary);
border-radius: var(--radius-md);
padding: var(--spacing-md);
margin-bottom: var(--spacing-md);
background: var(--bg-secondary);
}
.preamble-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-lg);
align-items: start;
}
.preamble-left {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.preamble-right {
display: flex;
flex-direction: column;
}
@media (max-width: 768px) {
.preamble-content {
grid-template-columns: 1fr;
}
}
.header-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--spacing-lg);
margin-bottom: var(--spacing-sm);
}
.header-label {
color: var(--text-secondary);
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.header-value {
font-size: 1.1rem;
font-weight: 600;
color: var(--text-primary);
}
.header-meta {
font-size: 0.8rem;
color: var(--text-secondary);
margin-top: 0.25rem;
}
.header-stat {
text-align: right;
}
.usage-pill {
display: inline-block;
padding: 0.2rem 0.6rem;
border-radius: 999px;
font-size: 0.8rem;
font-weight: 600;
}
.usage-pill.ok {
background: rgba(166, 226, 46, 0.2);
color: var(--color-success);
}
.usage-pill.medium {
background: rgba(253, 151, 31, 0.2);
color: var(--color-warning);
}
.usage-pill.high {
background: rgba(249, 38, 114, 0.2);
color: var(--color-danger);
}
.usage-pill.muted {
background: var(--bg-tertiary);
color: var(--text-secondary);
}
.inline-form,
.links-form,
.tag-form {
display: flex;
gap: var(--spacing-sm);
align-items: center;
margin-bottom: var(--spacing-sm);
}
.inline-form input,
.links-form input,
.tag-form input {
flex: 1;
padding: 0.5rem 0.75rem;
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
background: var(--bg-primary);
color: var(--text-primary);
}
.link-item {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--spacing-sm);
padding: 0.75rem;
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
background: var(--bg-primary);
}
/* Device list actions */
.device-list-item {
position: relative;
}
.device-list-delete {
background: transparent;
border: none;
color: var(--color-danger);
cursor: pointer;
font-size: 0.9rem;
padding: 0.2rem;
transition: transform 0.2s ease;
position: relative;
z-index: 10;
pointer-events: auto;
}
.device-list-delete:hover {
transform: scale(1.2);
filter: brightness(1.3);
}
/* Markdown blocks */
.markdown-block {
background: var(--bg-primary);
border: 1px solid var(--bg-tertiary);
border-radius: var(--radius-sm);
padding: var(--spacing-sm);
white-space: pre-wrap;
line-height: 1.5;
}
.markdown-block code {
background: rgba(0,0,0,0.3);
padding: 0 0.25rem;
border-radius: 4px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
}
tbody tr {
transition: background-color 0.2s;
}

10
frontend/device_detail.html Normal file → Executable file
View File

@@ -6,6 +6,8 @@
<title>Device Detail - Linux BenchTools</title>
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/components.css">
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon/icons8-devices-3d-fluency-32.png">
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon/icons8-devices-3d-fluency-16.png">
</head>
<body>
<!-- Header -->
@@ -32,12 +34,15 @@
<div id="deviceContent" style="display: none;">
<!-- Device Header -->
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: start;">
<div style="display: flex; justify-content: space-between; align-items: start; gap: 1rem;">
<div>
<h2 id="deviceHostname" style="color: var(--color-success); margin-bottom: 0.5rem;">--</h2>
<p id="deviceDescription" style="color: var(--text-secondary);">--</p>
</div>
<div id="globalScoreContainer"></div>
<div style="display: flex; gap: 0.75rem; align-items: flex-start;">
<div id="globalScoreContainer"></div>
<button id="deleteDeviceBtn" class="btn btn-danger btn-sm">🗑️ Supprimer</button>
</div>
</div>
<div id="deviceMeta" style="margin-top: 1rem; display: flex; gap: 1.5rem; flex-wrap: wrap;"></div>
@@ -214,6 +219,7 @@
</div>
<!-- Scripts -->
<script src="config.js"></script>
<script src="js/utils.js"></script>
<script src="js/api.js"></script>
<script src="js/device_detail.js"></script>

19
frontend/devices.html Normal file → Executable file
View File

@@ -6,6 +6,8 @@
<title>Devices - Linux BenchTools</title>
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/components.css">
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon/icons8-devices-3d-fluency-32.png">
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon/icons8-devices-3d-fluency-16.png">
</head>
<body>
<!-- Compact Header -->
@@ -58,7 +60,24 @@
<p>&copy; 2025 Linux BenchTools - Self-hosted benchmarking tool</p>
</footer>
<!-- Modal for Benchmark Details -->
<div id="benchmarkModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Détails du Benchmark</h3>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body" id="benchmarkModalBody">
<div class="loading">Chargement...</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="BenchUtils.closeModal('benchmarkModal')">Fermer</button>
</div>
</div>
</div>
<!-- Scripts -->
<script src="config.js"></script>
<script src="js/utils.js"></script>
<script src="js/api.js"></script>
<script src="js/devices.js"></script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
frontend/icons/icons8-bios-94.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 908 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 965 B

BIN
frontend/icons/icons8-done-48.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 776 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
frontend/icons/icons8-gpu-64.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
frontend/icons/icons8-hdd-94.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
frontend/icons/icons8-pcie-48.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 936 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
frontend/icons/icons8-ram-64.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
frontend/icons/icons8-save-48.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
frontend/icons/icons8-ssd-94.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

5
frontend/index.html Normal file → Executable file
View File

@@ -6,6 +6,8 @@
<title>Linux BenchTools - Dashboard</title>
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/components.css">
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon/icons8-devices-3d-fluency-32.png">
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon/icons8-devices-3d-fluency-16.png">
</head>
<body>
<!-- Header -->
@@ -77,7 +79,7 @@
</p>
<div class="code-block">
<button class="copy-btn" onclick="copyBenchCommand()">Copier</button>
<code id="benchCommand">curl -s http://VOTRE_SERVEUR/scripts/bench.sh | bash -s -- --server http://VOTRE_SERVEUR:8007/api/benchmark --token YOUR_TOKEN</code>
<code id="benchCommand">Chargement...</code>
</div>
</div>
</section>
@@ -99,6 +101,7 @@
</footer>
<!-- Scripts -->
<script src="config.js"></script>
<script src="js/utils.js"></script>
<script src="js/api.js"></script>
<script src="js/dashboard.js"></script>

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
};

File diff suppressed because it is too large Load Diff

75
frontend/settings.html Normal file → Executable file
View File

@@ -6,6 +6,8 @@
<title>Settings - Linux BenchTools</title>
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/components.css">
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon/icons8-devices-3d-fluency-32.png">
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon/icons8-devices-3d-fluency-16.png">
</head>
<body>
<!-- Header -->
@@ -25,6 +27,78 @@
<!-- Main Content -->
<main class="container">
<!-- Display Preferences -->
<div class="card">
<div class="card-header">🎨 Préférences d'affichage</div>
<div class="card-body">
<div class="alert alert-info" style="margin-bottom: 1.5rem;">
Configurez les unités d'affichage pour les valeurs matérielles
</div>
<div class="form-group">
<label class="form-label">Unité de mémoire (RAM)</label>
<select id="memoryUnit" class="form-control">
<option value="MB">Mégaoctets (MB)</option>
<option value="GB" selected>Gigaoctets (GB)</option>
</select>
<small style="color: var(--text-muted);">Affiche la RAM en MB ou GB dans les sections matérielles</small>
</div>
<div class="form-group">
<label class="form-label">Unité de stockage (Disques)</label>
<select id="storageUnit" class="form-control">
<option value="MB">Mégaoctets (MB)</option>
<option value="GB" selected>Gigaoctets (GB)</option>
<option value="TB">Téraoctets (TB)</option>
</select>
<small style="color: var(--text-muted);">Affiche la capacité des disques en MB, GB ou TB</small>
</div>
<div class="form-group">
<label class="form-label">Unité de cache CPU</label>
<select id="cacheUnit" class="form-control">
<option value="KB" selected>Kilooctets (KB)</option>
<option value="MB">Mégaoctets (MB)</option>
</select>
<small style="color: var(--text-muted);">Affiche les caches L1/L2/L3 en KB ou MB</small>
</div>
<div class="form-group">
<label class="form-label">Unité de température</label>
<select id="temperatureUnit" class="form-control">
<option value="C" selected>Celsius (°C)</option>
<option value="F">Fahrenheit (°F)</option>
</select>
<small style="color: var(--text-muted);">Affiche la température des composants en Celsius ou Fahrenheit</small>
</div>
<div class="form-group">
<label class="form-label">Taille des icônes de section</label>
<select id="sectionIconSize" class="form-control">
<option value="24">Petite (24px)</option>
<option value="28">Moyenne (28px)</option>
<option value="32" selected>Grande (32px)</option>
<option value="36">Très grande (36px)</option>
</select>
<small style="color: var(--text-muted);">Taille des icônes dans les titres de sections</small>
</div>
<div class="form-group">
<label class="form-label">Taille des icônes de bouton</label>
<select id="buttonIconSize" class="form-control">
<option value="18">Petite (18px)</option>
<option value="22">Moyenne (22px)</option>
<option value="24" selected>Grande (24px)</option>
<option value="28">Très grande (28px)</option>
</select>
<small style="color: var(--text-muted);">Taille des icônes dans les boutons d'action</small>
</div>
<button class="btn btn-primary" onclick="saveDisplayPreferences()">💾 Enregistrer les préférences</button>
<button class="btn btn-secondary" onclick="resetDisplayPreferences()" style="margin-left: 0.5rem;">🔄 Réinitialiser</button>
</div>
</div>
<!-- Bench Script Configuration -->
<div class="card">
<div class="card-header">⚡ Configuration Benchmark Script</div>
@@ -185,6 +259,7 @@
</footer>
<!-- Scripts -->
<script src="config.js"></script>
<script src="js/utils.js"></script>
<script src="js/api.js"></script>
<script src="js/settings.js"></script>