addon
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
933
frontend/js/peripheral-detail.js
Executable 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
1262
frontend/js/peripherals.js
Executable file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user