This commit is contained in:
Gilles Soulier
2026-01-05 16:08:01 +01:00
parent dcba044cd6
commit c67befc549
2215 changed files with 26743 additions and 329 deletions

View File

@@ -218,6 +218,13 @@ class BenchAPI {
async getStats() {
return this.get('/stats');
}
// ==================== Backup ====================
// Create database backup
async backupDatabase() {
return this.request('/backup', { method: 'POST' });
}
}
// Create global API instance

View File

@@ -1,7 +1,8 @@
// Linux BenchTools - Dashboard Logic
const { formatDate, formatRelativeTime, createScoreBadge, getScoreBadgeText, escapeHtml, showError, showEmptyState, copyToClipboard, showToast, debounce } = window.BenchUtils;
const api = window.BenchAPI;
// Access utilities and API
const utils = window.BenchUtils;
const apiClient = window.BenchAPI;
// Global state
let allDevices = [];
@@ -16,11 +17,21 @@ async function loadBackendConfig() {
if (response.ok) {
const config = await response.json();
apiToken = config.api_token;
iperfServer = config.iperf_server || '10.0.1.97';
iperfServer = config.iperf_server || '10.0.0.50';
updateBenchCommandDisplay();
} else {
console.error('Failed to load backend config - HTTP', response.status);
// Set default values to allow the page to render
apiToken = 'LOADING_ERROR';
iperfServer = '10.0.0.50';
updateBenchCommandDisplay();
}
} catch (error) {
console.error('Failed to load backend config:', error);
// Set default values to allow the page to render
apiToken = 'LOADING_ERROR';
iperfServer = '10.0.0.50';
updateBenchCommandDisplay();
}
}
@@ -42,7 +53,7 @@ async function loadDashboard() {
} catch (error) {
console.error('Failed to load dashboard:', error);
showToast('Erreur lors du chargement des données', 'error');
utils.showToast('Erreur lors du chargement des données', 'error');
} finally {
isLoading = false;
updateRefreshButton(false);
@@ -63,6 +74,28 @@ function updateRefreshButton(loading) {
}
}
async function backupDatabase() {
const btn = document.getElementById('backupBtn');
if (btn) {
btn.disabled = true;
btn.textContent = '💾 Backup...';
}
try {
const result = await apiClient.backupDatabase();
const files = (result.backups || []).map(b => b.filename).join(', ');
utils.showToast(`Backup créé${files ? `: ${files}` : ''}`, 'success');
} catch (error) {
console.error('Backup failed:', error);
utils.showToast(`Backup échoué: ${error.message}`, 'error');
} finally {
if (btn) {
btn.disabled = false;
btn.textContent = '💾 Backup DB';
}
}
}
// Update last refresh time
function updateLastRefreshTime() {
const element = document.getElementById('lastUpdate');
@@ -75,7 +108,7 @@ function updateLastRefreshTime() {
// Load statistics
async function loadStats() {
try {
const devices = await api.getDevices({ page_size: 1000 });
const devices = await apiClient.getDevices({ page_size: 100 });
const totalDevices = devices.total || 0;
let totalBenchmarks = 0;
@@ -107,7 +140,7 @@ async function loadStats() {
document.getElementById('totalBenchmarks').textContent = totalBenchmarks;
document.getElementById('avgScore').textContent = avgScore;
document.getElementById('lastBench').textContent = lastBenchDate
? formatRelativeTime(lastBenchDate.toISOString())
? utils.formatRelativeTime(lastBenchDate.toISOString())
: 'Aucun';
} catch (error) {
@@ -125,10 +158,10 @@ async function loadTopDevices() {
const container = document.getElementById('devicesTable');
try {
const data = await api.getDevices({ page_size: 50 });
const data = await apiClient.getDevices({ page_size: 50 });
if (!data.items || data.items.length === 0) {
showEmptyState(container, 'Aucun device trouvé. Exécutez un benchmark sur une machine pour commencer.', '📊');
utils.showEmptyState(container, 'Aucun device trouvé. Exécutez un benchmark sur une machine pour commencer.', '📊');
allDevices = [];
return;
}
@@ -151,7 +184,7 @@ async function loadTopDevices() {
container.innerHTML = `
<div class="error" style="text-align: center;">
<p style="margin-bottom: 1rem;">❌ Impossible de charger les devices</p>
<p style="font-size: 0.9rem; margin-bottom: 1rem;">${escapeHtml(error.message)}</p>
<p style="font-size: 0.9rem; margin-bottom: 1rem;">${utils.escapeHtml(error.message)}</p>
<button class="btn btn-primary btn-sm" onclick="loadTopDevices()">🔄 Réessayer</button>
</div>
`;
@@ -210,26 +243,26 @@ function createDeviceRow(device, rank) {
const runAt = bench?.run_at;
const globalScoreHtml = globalScore !== null && globalScore !== undefined
? `<span class="${window.BenchUtils.getScoreBadgeClass(globalScore)}">${getScoreBadgeText(globalScore)}</span>`
? `<span class="${utils.getScoreBadgeClass(globalScore)}">${utils.getScoreBadgeText(globalScore)}</span>`
: '<span class="badge">N/A</span>';
return `
<tr onclick="window.location.href='device_detail.html?id=${device.id}'">
<td><strong>${rank}</strong></td>
<td>
<strong style="color: var(--color-success);">${escapeHtml(device.hostname)}</strong>
<strong style="color: var(--color-success);">${utils.escapeHtml(device.hostname)}</strong>
</td>
<td style="color: var(--text-secondary);">
${escapeHtml(device.description || 'Aucune description')}
${utils.escapeHtml(device.description || 'Aucune description')}
</td>
<td>${globalScoreHtml}</td>
<td><span class="${window.BenchUtils.getScoreBadgeClass(cpuScore)}">${getScoreBadgeText(cpuScore)}</span></td>
<td><span class="${window.BenchUtils.getScoreBadgeClass(memScore)}">${getScoreBadgeText(memScore)}</span></td>
<td><span class="${window.BenchUtils.getScoreBadgeClass(diskScore)}">${getScoreBadgeText(diskScore)}</span></td>
<td><span class="${window.BenchUtils.getScoreBadgeClass(netScore)}">${getScoreBadgeText(netScore)}</span></td>
<td><span class="${window.BenchUtils.getScoreBadgeClass(gpuScore)}">${getScoreBadgeText(gpuScore)}</span></td>
<td><span class="${utils.getScoreBadgeClass(cpuScore)}">${utils.getScoreBadgeText(cpuScore)}</span></td>
<td><span class="${utils.getScoreBadgeClass(memScore)}">${utils.getScoreBadgeText(memScore)}</span></td>
<td><span class="${utils.getScoreBadgeClass(diskScore)}">${utils.getScoreBadgeText(diskScore)}</span></td>
<td><span class="${utils.getScoreBadgeClass(netScore)}">${utils.getScoreBadgeText(netScore)}</span></td>
<td><span class="${utils.getScoreBadgeClass(gpuScore)}">${utils.getScoreBadgeText(gpuScore)}</span></td>
<td style="color: var(--text-secondary); font-size: 0.85rem;">
${runAt ? formatRelativeTime(runAt) : 'Jamais'}
${runAt ? utils.formatRelativeTime(runAt) : 'Jamais'}
</td>
<td>
<a href="device_detail.html?id=${device.id}" class="btn btn-sm btn-primary">Voir</a>
@@ -244,7 +277,7 @@ function buildBenchCommand() {
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';
const iperf = iperfServer || '10.0.0.50';
// Extract backend URL without /api suffix
const backendUrl = backendBase.replace(/\/api$/, '');
@@ -261,12 +294,12 @@ function updateBenchCommandDisplay() {
// Copy bench command to clipboard
async function copyBenchCommand() {
const command = document.getElementById('benchCommand').textContent;
const success = await copyToClipboard(command);
const success = await utils.copyToClipboard(command);
if (success) {
showToast('Commande copiée dans le presse-papier !', 'success');
utils.showToast('Commande copiée dans le presse-papier !', 'success');
} else {
showToast('Erreur lors de la copie', 'error');
utils.showToast('Erreur lors de la copie', 'error');
}
}
@@ -292,7 +325,7 @@ function filterDevices(query) {
}
// Debounced search
const debouncedSearch = debounce((query) => {
const debouncedSearch = utils.debounce((query) => {
filterDevices(query);
}, 300);

933
frontend/js/peripheral-detail.js Executable file
View File

@@ -0,0 +1,933 @@
/**
* Linux BenchTools - Peripheral Detail Page
*/
let peripheralId = null;
let peripheral = null;
let editMode = false;
// Initialize
document.addEventListener('DOMContentLoaded', () => {
// Get peripheral ID from URL
const urlParams = new URLSearchParams(window.location.search);
peripheralId = parseInt(urlParams.get('id'));
if (!peripheralId) {
showError('ID périphérique invalide');
setTimeout(() => window.location.href = 'peripherals.html', 2000);
return;
}
loadPeripheral();
loadPhotos();
loadDocuments();
loadLinks();
loadHistory();
const photoUrlInput = document.getElementById('photo-url');
if (photoUrlInput) {
photoUrlInput.addEventListener('input', updatePhotoUrlButton);
}
const editUtilisation = document.getElementById('edit-utilisation');
if (editUtilisation) {
editUtilisation.addEventListener('change', updateEditUtilisationFields);
}
});
// Load peripheral details
async function loadPeripheral() {
try {
peripheral = await apiRequest(`/peripherals/${peripheralId}`);
displayPeripheral(peripheral);
} catch (error) {
console.error('Error loading peripheral:', error);
showError('Erreur lors du chargement du périphérique');
}
}
// Display peripheral information
function displayPeripheral(p) {
// Update page title
document.getElementById('peripheral-name').textContent = p.nom;
document.title = `${p.nom} - Linux BenchTools`;
// Main info
document.getElementById('type_principal').textContent = p.type_principal || '-';
document.getElementById('sous_type').textContent = p.sous_type || '-';
document.getElementById('marque').textContent = p.marque || '-';
document.getElementById('modele').textContent = p.modele || '-';
document.getElementById('numero_serie').textContent = p.numero_serie || '-';
// USB info - show only if present
if (p.vendor_id) {
document.getElementById('vendor_id').textContent = p.vendor_id;
document.getElementById('vendor-id-item').style.display = 'block';
} else {
document.getElementById('vendor-id-item').style.display = 'none';
}
if (p.product_id) {
document.getElementById('product_id').textContent = p.product_id;
document.getElementById('product-id-item').style.display = 'block';
} else {
document.getElementById('product-id-item').style.display = 'none';
}
if (p.usb_device_id) {
document.getElementById('usb_device_id').textContent = p.usb_device_id;
document.getElementById('usb-device-id-item').style.display = 'block';
} else {
document.getElementById('usb-device-id-item').style.display = 'none';
}
if (p.fabricant) {
document.getElementById('fabricant').textContent = p.fabricant;
document.getElementById('fabricant-item').style.display = 'block';
} else {
document.getElementById('fabricant-item').style.display = 'none';
}
if (p.produit) {
document.getElementById('produit').textContent = p.produit;
document.getElementById('produit-item').style.display = 'block';
} else {
document.getElementById('produit-item').style.display = 'none';
}
const etatSpan = document.getElementById('etat');
if (p.etat) {
etatSpan.innerHTML = `<span class="badge badge-${getEtatClass(p.etat)}">${p.etat}</span>`;
} else {
etatSpan.textContent = '-';
}
document.getElementById('rating').innerHTML = renderStars(p.rating || 0);
document.getElementById('quantite_disponible').textContent =
`${p.quantite_disponible || 0} / ${p.quantite_totale || 0}`;
// Purchase info
document.getElementById('boutique').textContent = p.boutique || '-';
document.getElementById('date_achat').textContent = p.date_achat ? formatDate(p.date_achat) : '-';
document.getElementById('prix').textContent = p.prix ? `${p.prix.toFixed(2)} ${p.devise || 'EUR'}` : '-';
let garantieText = '-';
if (p.garantie_duree_mois) {
garantieText = `${p.garantie_duree_mois} mois`;
if (p.garantie_expiration) {
garantieText += ` (exp: ${formatDate(p.garantie_expiration)})`;
}
}
document.getElementById('garantie').textContent = garantieText;
// Location
if (p.location_id) {
document.getElementById('location').textContent = `Location #${p.location_id}`;
} else if (p.location_details) {
document.getElementById('location').textContent = p.location_details;
} else {
document.getElementById('location').textContent = '-';
}
document.getElementById('location_details').textContent = p.location_details || '-';
if (p.connecte_a) {
document.getElementById('connecte_a').textContent = p.connecte_a;
document.getElementById('connecte_a-item').style.display = 'block';
} else {
document.getElementById('connecte_a-item').style.display = 'none';
}
// Notes
document.getElementById('notes').textContent = p.notes || 'Aucune note';
attachCopyButtons();
}
// Load photos
async function loadPhotos() {
try {
const photos = await apiRequest(`/peripherals/${peripheralId}/photos`);
displayPhotos(photos);
} catch (error) {
console.error('Error loading photos:', error);
}
}
// Display photos
function displayPhotos(photos) {
const grid = document.getElementById('photos-grid');
if (photos.length === 0) {
grid.innerHTML = '<p class="text-muted">Aucune photo</p>';
return;
}
grid.innerHTML = photos.map(photo => `
<div class="photo-item">
<img src="${photo.stored_path}" alt="${escapeHtml(photo.description || 'Photo')}">
${photo.is_primary ? '<span class="badge badge-primary"><i class="fas fa-star"></i> Principale</span>' : ''}
<button class="photo-primary-toggle ${photo.is_primary ? 'active' : ''}"
onclick="setPrimaryPhoto(${photo.id})"
title="${photo.is_primary ? 'Photo principale' : 'Définir comme photo principale'}">
<i class="fas fa-${photo.is_primary ? 'check-circle' : 'circle'}"></i>
</button>
<div class="photo-actions">
<button class="btn-icon" onclick="deletePhoto(${photo.id})" title="Supprimer">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`).join('');
}
// Load documents
async function loadDocuments() {
try {
const documents = await apiRequest(`/peripherals/${peripheralId}/documents`);
displayDocuments(documents);
} catch (error) {
console.error('Error loading documents:', error);
}
}
// Display documents
function displayDocuments(documents) {
const list = document.getElementById('documents-list');
if (documents.length === 0) {
list.innerHTML = '<p class="text-muted">Aucun document</p>';
return;
}
list.innerHTML = documents.map(doc => `
<div class="document-item">
<div class="document-icon">
<i class="fas fa-file-${getDocIcon(doc.doc_type)}"></i>
</div>
<div class="document-info">
<strong>${escapeHtml(doc.filename)}</strong>
<span class="text-muted">${getDocTypeLabel(doc.doc_type)} - ${formatBytes(doc.size_bytes || 0)}</span>
${doc.description ? `<p>${escapeHtml(doc.description)}</p>` : ''}
</div>
<div class="document-actions">
<a href="${doc.stored_path}" target="_blank" class="btn-icon" title="Télécharger">
<i class="fas fa-download"></i>
</a>
<button class="btn-icon" onclick="deleteDocument(${doc.id})" title="Supprimer">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`).join('');
}
// Load links
async function loadLinks() {
try {
const links = await apiRequest(`/peripherals/${peripheralId}/links`);
displayLinks(links);
} catch (error) {
console.error('Error loading links:', error);
}
}
// Display links
function displayLinks(links) {
const list = document.getElementById('links-list');
if (links.length === 0) {
list.innerHTML = '<p class="text-muted">Aucun lien</p>';
return;
}
list.innerHTML = links.map(link => `
<div class="link-item">
<div class="link-icon">
<i class="fas fa-${getLinkIcon(link.link_type)}"></i>
</div>
<div class="link-info">
<strong>${escapeHtml(link.label)}</strong>
<span class="text-muted">${getLinkTypeLabel(link.link_type)}</span>
<a href="${escapeHtml(link.url)}" target="_blank" class="link-url">${escapeHtml(link.url)}</a>
</div>
<div class="link-actions">
<button class="btn-icon" onclick="deleteLink(${link.id})" title="Supprimer">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`).join('');
}
// Load history
async function loadHistory() {
try {
const history = await apiRequest(`/peripherals/${peripheralId}/history`);
displayHistory(history);
} catch (error) {
console.error('Error loading history:', error);
}
}
// Display history
function displayHistory(history) {
const list = document.getElementById('history-list');
if (history.length === 0) {
list.innerHTML = '<p class="text-muted">Aucun historique</p>';
return;
}
list.innerHTML = `
<div class="history-timeline">
${history.map(h => `
<div class="history-item">
<div class="history-icon">
<i class="fas fa-${getHistoryIcon(h.action)}"></i>
</div>
<div class="history-content">
<strong>${getHistoryActionLabel(h.action)}</strong>
<span class="text-muted">${formatDateTime(h.timestamp)}</span>
${h.user ? `<span class="text-muted">par ${escapeHtml(h.user)}</span>` : ''}
${h.notes ? `<p>${escapeHtml(h.notes)}</p>` : ''}
</div>
</div>
`).join('')}
</div>
`;
}
// Set photo as primary
async function setPrimaryPhoto(photoId) {
try {
await apiRequest(`/peripherals/${peripheralId}/photos/${photoId}/set-primary`, {
method: 'POST'
});
showSuccess('Photo principale définie');
loadPhotos(); // Reload to update icons
} catch (error) {
console.error('Error setting primary photo:', error);
showError('Erreur lors de la définition de la photo principale');
}
}
// Upload photo
async function uploadPhoto(event) {
event.preventDefault();
const formData = new FormData(event.target);
const file = document.getElementById('photo-file').files[0];
const imageUrl = (document.getElementById('photo-url').value || '').trim();
if (!file && !imageUrl) {
showError('Veuillez choisir un fichier ou fournir une URL');
return;
}
if (imageUrl && !file) {
await uploadPhotoFromUrl();
return;
}
try {
await apiRequest(`/peripherals/${peripheralId}/photos`, {
method: 'POST',
body: formData
});
closeModal('modal-upload-photo');
showSuccess('Photo ajoutée avec succès');
loadPhotos();
} catch (error) {
console.error('Error uploading photo:', error);
showError('Erreur lors de l\'upload de la photo');
}
}
function updatePhotoUrlButton() {
const imageUrl = (document.getElementById('photo-url').value || '').trim();
const button = document.getElementById('btn-upload-url');
if (!button) return;
button.disabled = !/^https?:\/\//i.test(imageUrl);
}
async function uploadPhotoFromUrl() {
const imageUrl = (document.getElementById('photo-url').value || '').trim();
if (!/^https?:\/\//i.test(imageUrl)) {
showError('URL invalide (http/https requis)');
return;
}
const formData = new FormData();
formData.append('image_url', imageUrl);
formData.append('description', document.getElementById('photo-description').value || '');
formData.append('is_primary', document.getElementById('photo-primary').checked ? 'true' : 'false');
try {
await apiRequest(`/peripherals/${peripheralId}/photos/from-url`, {
method: 'POST',
body: formData
});
closeModal('modal-upload-photo');
document.getElementById('form-upload-photo').reset();
updatePhotoUrlButton();
showSuccess('Photo importée depuis URL');
loadPhotos();
} catch (error) {
console.error('Error importing photo from URL:', error);
showError(error.message || 'Erreur lors de l\'import de l\'URL');
}
}
// Upload document
async function uploadDocument(event) {
event.preventDefault();
const formData = new FormData(event.target);
try {
await apiRequest(`/peripherals/${peripheralId}/documents`, {
method: 'POST',
body: formData
});
closeModal('modal-upload-document');
showSuccess('Document ajouté avec succès');
loadDocuments();
} catch (error) {
console.error('Error uploading document:', error);
showError('Erreur lors de l\'upload du document');
}
}
// Add link
async function addLink(event) {
event.preventDefault();
const formData = new FormData(event.target);
const data = {
peripheral_id: peripheralId
};
for (let [key, value] of formData.entries()) {
data[key] = value;
}
try {
await apiRequest(`/peripherals/${peripheralId}/links`, {
method: 'POST',
body: JSON.stringify(data)
});
closeModal('modal-add-link');
showSuccess('Lien ajouté avec succès');
loadLinks();
} catch (error) {
console.error('Error adding link:', error);
showError('Erreur lors de l\'ajout du lien');
}
}
// Delete functions
async function deletePhoto(photoId) {
if (!confirm('Supprimer cette photo ?')) return;
try {
await apiRequest(`/peripherals/photos/${photoId}`, { method: 'DELETE' });
showSuccess('Photo supprimée');
loadPhotos();
} catch (error) {
showError('Erreur lors de la suppression');
}
}
async function deleteDocument(docId) {
if (!confirm('Supprimer ce document ?')) return;
try {
await apiRequest(`/peripherals/documents/${docId}`, { method: 'DELETE' });
showSuccess('Document supprimé');
loadDocuments();
} catch (error) {
showError('Erreur lors de la suppression');
}
}
async function deleteLink(linkId) {
if (!confirm('Supprimer ce lien ?')) return;
try {
await apiRequest(`/peripherals/links/${linkId}`, { method: 'DELETE' });
showSuccess('Lien supprimé');
loadLinks();
} catch (error) {
showError('Erreur lors de la suppression');
}
}
async function deletePeripheral() {
if (!confirm('Êtes-vous sûr de vouloir supprimer ce périphérique ?')) return;
try {
await apiRequest(`/peripherals/${peripheralId}`, { method: 'DELETE' });
showSuccess('Périphérique supprimé');
setTimeout(() => window.location.href = 'peripherals.html', 1500);
} catch (error) {
showError('Erreur lors de la suppression');
}
}
// Modal functions
function showUploadPhotoModal() {
document.getElementById('modal-upload-photo').style.display = 'block';
}
function showUploadDocumentModal() {
document.getElementById('modal-upload-document').style.display = 'block';
}
function showAddLinkModal() {
document.getElementById('modal-add-link').style.display = 'block';
}
function closeModal(modalId) {
document.getElementById(modalId).style.display = 'none';
}
// Helper functions
function getEtatClass(etat) {
const classes = {
'Neuf': 'success',
'Bon': 'info',
'Usagé': 'warning',
'Défectueux': 'danger',
'Retiré': 'secondary'
};
return classes[etat] || 'secondary';
}
function renderStars(rating) {
const fullStars = Math.floor(rating);
const hasHalfStar = rating % 1 >= 0.5;
const emptyStars = 5 - fullStars - (hasHalfStar ? 1 : 0);
let html = '';
for (let i = 0; i < fullStars; i++) html += '<i class="fas fa-star text-warning"></i>';
if (hasHalfStar) html += '<i class="fas fa-star-half-alt text-warning"></i>';
for (let i = 0; i < emptyStars; i++) html += '<i class="far fa-star text-muted"></i>';
return html;
}
function attachCopyButtons() {
const items = document.querySelectorAll('.info-item');
items.forEach(item => {
const existing = item.querySelector('.copy-field-btn');
if (existing) existing.remove();
if (item.style.display === 'none') {
return;
}
const valueEl = item.querySelector('span');
if (!valueEl) return;
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'copy-field-btn';
btn.innerHTML = '<i class="fas fa-copy"></i><span class="tooltip-copied">Copié</span>';
btn.addEventListener('click', async (event) => {
event.stopPropagation();
const text = (valueEl.innerText || '').trim();
if (!text || text === '-') {
showError('Rien à copier');
return;
}
try {
await copyTextToClipboard(text);
btn.classList.add('copied');
setTimeout(() => btn.classList.remove('copied'), 1500);
showSuccess('Copié');
} catch (error) {
console.error('Copy failed:', error);
showError('Copie impossible');
}
});
item.appendChild(btn);
});
}
async function copyTextToClipboard(text) {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
return;
}
// Fallback for non-HTTPS / older browsers
const input = document.createElement('input');
input.value = text;
document.body.appendChild(input);
input.select();
input.setSelectionRange(0, input.value.length);
const ok = document.execCommand('copy');
document.body.removeChild(input);
if (!ok) {
throw new Error('execCommand copy failed');
}
}
function getDocIcon(docType) {
const icons = {
'manual': 'pdf',
'warranty': 'certificate',
'invoice': 'file-invoice',
'datasheet': 'file-code',
'other': 'file'
};
return icons[docType] || 'file';
}
function getDocTypeLabel(docType) {
const labels = {
'manual': 'Manuel',
'warranty': 'Garantie',
'invoice': 'Facture',
'datasheet': 'Fiche technique',
'other': 'Autre'
};
return labels[docType] || docType;
}
function getLinkIcon(linkType) {
const icons = {
'manufacturer': 'industry',
'support': 'life-ring',
'drivers': 'download',
'documentation': 'book',
'custom': 'link'
};
return icons[linkType] || 'link';
}
function getLinkTypeLabel(linkType) {
const labels = {
'manufacturer': 'Fabricant',
'support': 'Support',
'drivers': 'Drivers',
'documentation': 'Documentation',
'custom': 'Personnalisé'
};
return labels[linkType] || linkType;
}
function getHistoryIcon(action) {
const icons = {
'created': 'plus',
'moved': 'arrows-alt',
'assigned': 'link',
'unassigned': 'unlink',
'stored': 'box'
};
return icons[action] || 'circle';
}
function getHistoryActionLabel(action) {
const labels = {
'created': 'Créé',
'moved': 'Déplacé',
'assigned': 'Assigné',
'unassigned': 'Désassigné',
'stored': 'Stocké'
};
return labels[action] || action;
}
function toggleEditMode() {
if (!peripheral) {
showError('Aucun périphérique chargé');
return;
}
// Populate form with peripheral data
document.getElementById('edit-nom').value = peripheral.nom || '';
document.getElementById('edit-type_principal').value = peripheral.type_principal || '';
document.getElementById('edit-sous_type').value = peripheral.sous_type || '';
document.getElementById('edit-marque').value = peripheral.marque || '';
document.getElementById('edit-modele').value = peripheral.modele || '';
document.getElementById('edit-numero_serie').value = peripheral.numero_serie || '';
// USB info
document.getElementById('edit-vendor_id').value = peripheral.vendor_id || '';
document.getElementById('edit-product_id').value = peripheral.product_id || '';
document.getElementById('edit-usb_device_id').value = peripheral.usb_device_id || '';
document.getElementById('edit-fabricant').value = peripheral.fabricant || '';
document.getElementById('edit-produit').value = peripheral.produit || '';
document.getElementById('edit-boutique').value = peripheral.boutique || '';
document.getElementById('edit-date_achat').value = peripheral.date_achat || '';
document.getElementById('edit-prix').value = peripheral.prix || '';
document.getElementById('edit-devise').value = peripheral.devise || 'EUR';
document.getElementById('edit-garantie_duree_mois').value = peripheral.garantie_duree_mois || '';
document.getElementById('edit-etat').value = peripheral.etat || 'Neuf';
setEditRating(peripheral.rating || 0);
document.getElementById('edit-quantite_totale').value = peripheral.quantite_totale || 1;
document.getElementById('edit-quantite_disponible').value = peripheral.quantite_disponible || 1;
const utilisationValue = peripheral.connecte_a ? 'utilise' : 'non_utilise';
document.getElementById('edit-utilisation').value = utilisationValue;
document.getElementById('edit-synthese').value = peripheral.synthese || '';
document.getElementById('edit-cli_yaml').value = peripheral.cli_yaml || '';
document.getElementById('edit-cli_raw').value = peripheral.cli_raw || '';
document.getElementById('edit-specifications').value = peripheral.specifications || '';
document.getElementById('edit-notes').value = peripheral.notes || '';
// Load and set location
loadEditLocations(peripheral.location_id);
// Load and set boutique
loadEditBoutiques(peripheral.boutique);
// Load and set host
loadEditHosts(peripheral.connecte_a);
updateEditUtilisationFields();
loadEditStockageLocations(peripheral.location_details);
// Show modal
document.getElementById('modal-edit').style.display = 'block';
}
// Load hosts for edit modal
async function loadEditHosts(selectedHost) {
try {
const result = await apiRequest('/peripherals/config/hosts');
if (!result.success) return;
const select = document.getElementById('edit-connecte_a');
if (!select) return;
select.innerHTML = '<option value="">Non défini</option>';
result.hosts.forEach(host => {
const option = document.createElement('option');
option.value = host.nom;
option.textContent = host.localisation ? `${host.nom} (${host.localisation})` : host.nom;
if (host.nom === selectedHost) {
option.selected = true;
}
select.appendChild(option);
});
if (selectedHost && !result.hosts.find(h => h.nom === selectedHost)) {
const option = document.createElement('option');
option.value = selectedHost;
option.textContent = selectedHost;
option.selected = true;
select.appendChild(option);
}
} catch (error) {
console.error('Error loading hosts:', error);
}
}
// Load storage locations for edit modal
async function loadEditStockageLocations(selectedLocation) {
try {
const result = await apiRequest('/peripherals/config/stockage-locations');
if (!result.success) return;
const select = document.getElementById('edit-stockage_location');
if (!select) return;
select.innerHTML = '<option value="">Non défini</option>';
result.locations.forEach(name => {
const option = document.createElement('option');
option.value = name;
option.textContent = name;
if (name === selectedLocation) {
option.selected = true;
}
select.appendChild(option);
});
if (selectedLocation && !result.locations.includes(selectedLocation)) {
const option = document.createElement('option');
option.value = selectedLocation;
option.textContent = selectedLocation;
option.selected = true;
select.appendChild(option);
}
} catch (error) {
console.error('Error loading storage locations:', error);
}
}
function updateEditUtilisationFields() {
const utilisation = document.getElementById('edit-utilisation')?.value || 'non_utilise';
const hostGroup = document.getElementById('edit-group-connecte_a');
const locationGroup = document.getElementById('edit-group-location_id');
const stockageGroup = document.getElementById('edit-group-stockage_location');
if (utilisation === 'utilise') {
if (hostGroup) hostGroup.style.display = 'block';
if (locationGroup) locationGroup.style.display = 'none';
if (stockageGroup) stockageGroup.style.display = 'none';
} else {
if (hostGroup) hostGroup.style.display = 'none';
if (locationGroup) locationGroup.style.display = 'none';
if (stockageGroup) stockageGroup.style.display = 'block';
}
}
// Load boutiques for edit modal
async function loadEditBoutiques(selectedBoutique) {
try {
const result = await apiRequest('/peripherals/config/boutiques');
if (!result.success) return;
const select = document.getElementById('edit-boutique');
if (!select) return;
select.innerHTML = '<option value="">Non définie</option>';
result.boutiques.forEach(name => {
const option = document.createElement('option');
option.value = name;
option.textContent = name;
if (name === selectedBoutique) {
option.selected = true;
}
select.appendChild(option);
});
if (selectedBoutique && !result.boutiques.includes(selectedBoutique)) {
const option = document.createElement('option');
option.value = selectedBoutique;
option.textContent = selectedBoutique;
option.selected = true;
select.appendChild(option);
}
} catch (error) {
console.error('Error loading boutiques:', error);
}
}
// Load locations for edit modal
async function loadEditLocations(selectedLocationId) {
try {
const locations = await apiRequest('/locations/');
const select = document.getElementById('edit-location_id');
select.innerHTML = '<option value="">Non définie</option>';
locations.forEach(location => {
const option = document.createElement('option');
option.value = location.id;
option.textContent = location.nom;
if (location.id === selectedLocationId) {
option.selected = true;
}
select.appendChild(option);
});
} catch (error) {
console.error('Error loading locations:', error);
}
}
function closeEditModal() {
document.getElementById('modal-edit').style.display = 'none';
}
function setEditRating(rating) {
const stars = document.querySelectorAll('#edit-star-rating .fa-star');
const ratingInput = document.getElementById('edit-rating');
ratingInput.value = rating;
stars.forEach((star, index) => {
if (index < rating) {
star.classList.add('active');
} else {
star.classList.remove('active');
}
});
}
// Star rating click handler for edit form
document.addEventListener('DOMContentLoaded', () => {
const editStars = document.querySelectorAll('#edit-star-rating .fa-star');
editStars.forEach(star => {
star.addEventListener('click', () => {
const rating = parseInt(star.getAttribute('data-rating'));
setEditRating(rating);
});
});
});
async function savePeripheral(event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
const data = {};
// Convert FormData to object
for (let [key, value] of formData.entries()) {
// Convert numeric fields
if (key === 'utilisation') {
continue;
} else if (['prix', 'garantie_duree_mois', 'quantite_totale', 'quantite_disponible', 'rating'].includes(key)) {
data[key] = value ? parseFloat(value) : null;
} else {
data[key] = value || null;
}
}
const utilisation = document.getElementById('edit-utilisation')?.value || 'non_utilise';
if (utilisation === 'utilise') {
data.connecte_a = document.getElementById('edit-connecte_a')?.value || null;
data.location_id = null;
const hostSelect = document.getElementById('edit-connecte_a');
const hostText = hostSelect?.selectedOptions?.[0]?.textContent || '';
const match = hostText.match(/\((.+)\)$/);
if (match && match[1]) {
data.location_details = match[1];
}
} else {
data.connecte_a = null;
data.location_details = null;
data.location_id = null;
const stockage = document.getElementById('edit-stockage_location')?.value || null;
data.location_details = stockage;
}
try {
const response = await apiRequest(`/peripherals/${peripheralId}`, {
method: 'PUT',
body: JSON.stringify(data)
});
showSuccess('Périphérique mis à jour avec succès');
closeEditModal();
// Reload peripheral data
await loadPeripheral();
} catch (error) {
console.error('Error updating peripheral:', error);
showError('Erreur lors de la mise à jour du périphérique');
}
}
// Close modal when clicking outside
window.onclick = function(event) {
if (event.target.classList.contains('modal')) {
event.target.style.display = 'none';
}
}

1262
frontend/js/peripherals.js Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -100,27 +100,36 @@ function escapeHtml(text) {
// 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 modern clipboard API first
if (navigator.clipboard && navigator.clipboard.writeText) {
try {
document.execCommand('copy');
document.body.removeChild(textArea);
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
document.body.removeChild(textArea);
return false;
console.error('Clipboard API failed:', err);
// Fall through to fallback method
}
}
// Fallback for older browsers or non-HTTPS contexts
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '0';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand('copy');
document.body.removeChild(textArea);
return successful;
} catch (err) {
console.error('Fallback copy failed:', err);
document.body.removeChild(textArea);
return false;
}
}
// Show toast notification
@@ -432,6 +441,75 @@ function renderMarkdown(text) {
return `<div class="markdown-block">${html}</div>`;
}
// Additional utility functions for peripherals module
function formatDateTime(dateString) {
if (!dateString) return 'N/A';
const date = new Date(dateString);
return date.toLocaleString('fr-FR', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
function formatBytes(bytes) {
return formatFileSize(bytes);
}
function showSuccess(message) {
showToast(message, 'success');
}
function showInfo(message) {
showToast(message, 'info');
}
// API request helper
async function apiRequest(endpoint, options = {}) {
const config = window.CONFIG || { API_BASE_URL: 'http://localhost:8007/api' };
const url = `${config.API_BASE_URL}${endpoint}`;
const defaultOptions = {
headers: {}
};
// Don't set Content-Type for FormData (browser will set it with boundary)
if (options.body && !(options.body instanceof FormData)) {
defaultOptions.headers['Content-Type'] = 'application/json';
}
const response = await fetch(url, {
...defaultOptions,
...options,
headers: {
...defaultOptions.headers,
...options.headers
}
});
if (!response.ok) {
let message = `HTTP ${response.status}: ${response.statusText}`;
try {
const data = await response.json();
if (data && typeof data === 'object') {
message = data.detail || data.message || message;
}
} catch (error) {
// Ignore JSON parse errors and keep default message.
}
throw new Error(message);
}
// Handle 204 No Content
if (response.status === 204) {
return null;
}
return response.json();
}
window.BenchUtils = {
formatDate,
formatRelativeTime,
@@ -461,5 +539,10 @@ window.BenchUtils = {
formatStorage,
formatCache,
formatTemperature,
renderMarkdown
renderMarkdown,
formatDateTime,
formatBytes,
showSuccess,
showInfo,
apiRequest
};