546 lines
20 KiB
JavaScript
546 lines
20 KiB
JavaScript
// Linux BenchTools - Device Detail Logic
|
|
|
|
const { formatDate, formatRelativeTime, formatFileSize, createScoreBadge, getScoreBadgeText, escapeHtml, showError, showEmptyState, formatTags, initTabs, openModal, showToast, formatHardwareInfo } = window.BenchUtils;
|
|
const api = window.BenchAPI;
|
|
|
|
let currentDeviceId = null;
|
|
let currentDevice = null;
|
|
|
|
// Initialize page
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
// Get device ID from URL
|
|
currentDeviceId = window.BenchUtils.getUrlParameter('id');
|
|
|
|
if (!currentDeviceId) {
|
|
document.getElementById('loadingState').innerHTML = '<div class="error">Device ID manquant dans l\'URL</div>';
|
|
return;
|
|
}
|
|
|
|
// Initialize tabs
|
|
initTabs('.tabs-container');
|
|
|
|
// Load device data
|
|
await loadDeviceDetail();
|
|
});
|
|
|
|
// Load device detail
|
|
async function loadDeviceDetail() {
|
|
try {
|
|
currentDevice = await api.getDevice(currentDeviceId);
|
|
|
|
// Show content, hide loading
|
|
document.getElementById('loadingState').style.display = 'none';
|
|
document.getElementById('deviceContent').style.display = 'block';
|
|
|
|
// Render all sections
|
|
renderDeviceHeader();
|
|
renderHardwareSummary();
|
|
renderLastBenchmark();
|
|
renderNetworkDetails();
|
|
await loadBenchmarkHistory();
|
|
await loadDocuments();
|
|
await loadLinks();
|
|
|
|
} catch (error) {
|
|
console.error('Failed to load device:', error);
|
|
document.getElementById('loadingState').innerHTML =
|
|
`<div class="error">Erreur lors du chargement du device: ${escapeHtml(error.message)}</div>`;
|
|
}
|
|
}
|
|
|
|
// Render device header
|
|
function renderDeviceHeader() {
|
|
document.getElementById('deviceHostname').textContent = currentDevice.hostname;
|
|
document.getElementById('deviceDescription').textContent = currentDevice.description || 'Aucune description';
|
|
|
|
// Global score
|
|
const globalScore = currentDevice.last_benchmark?.global_score;
|
|
document.getElementById('globalScoreContainer').innerHTML =
|
|
globalScore !== null && globalScore !== undefined
|
|
? `<div class="${window.BenchUtils.getScoreBadgeClass(globalScore)}" style="font-size: 2rem; min-width: 80px; height: 80px; display: flex; align-items: center; justify-content: center;">${getScoreBadgeText(globalScore)}</div>`
|
|
: '<span class="badge">N/A</span>';
|
|
|
|
// Meta information
|
|
const metaParts = [];
|
|
if (currentDevice.location) metaParts.push(`📍 ${escapeHtml(currentDevice.location)}`);
|
|
if (currentDevice.owner) metaParts.push(`👤 ${escapeHtml(currentDevice.owner)}`);
|
|
if (currentDevice.asset_tag) metaParts.push(`🏷️ ${escapeHtml(currentDevice.asset_tag)}`);
|
|
if (currentDevice.last_benchmark?.run_at) metaParts.push(`⏱️ ${formatRelativeTime(currentDevice.last_benchmark.run_at)}`);
|
|
|
|
document.getElementById('deviceMeta').innerHTML = metaParts.map(part =>
|
|
`<span style="color: var(--text-secondary);">${part}</span>`
|
|
).join('');
|
|
|
|
// Tags
|
|
if (currentDevice.tags) {
|
|
document.getElementById('deviceTags').innerHTML = formatTags(currentDevice.tags);
|
|
}
|
|
}
|
|
|
|
// Render hardware summary
|
|
function renderHardwareSummary() {
|
|
const snapshot = currentDevice.last_hardware_snapshot;
|
|
|
|
if (!snapshot) {
|
|
document.getElementById('hardwareSummary').innerHTML =
|
|
'<p style="color: var(--text-muted);">Aucune information hardware disponible</p>';
|
|
return;
|
|
}
|
|
|
|
// RAM usage info
|
|
const ramTotalGB = Math.round((snapshot.ram_total_mb || 0) / 1024);
|
|
const ramUsedMB = snapshot.ram_used_mb || 0;
|
|
const ramFreeMB = snapshot.ram_free_mb || 0;
|
|
const ramSharedMB = snapshot.ram_shared_mb || 0;
|
|
|
|
let ramValue = `${ramTotalGB} GB`;
|
|
if (ramUsedMB > 0 || ramFreeMB > 0) {
|
|
const usagePercent = ramTotalGB > 0 ? Math.round((ramUsedMB / (snapshot.ram_total_mb || 1)) * 100) : 0;
|
|
ramValue = `${ramTotalGB} GB (${usagePercent}% utilisé)<br><small>Utilisée: ${Math.round(ramUsedMB / 1024)}GB • Libre: ${Math.round(ramFreeMB / 1024)}GB`;
|
|
if (ramSharedMB > 0) {
|
|
ramValue += ` • Partagée: ${Math.round(ramSharedMB / 1024)}GB`;
|
|
}
|
|
ramValue += `<br>${snapshot.ram_slots_used || '?'} / ${snapshot.ram_slots_total || '?'} slots</small>`;
|
|
} else {
|
|
ramValue += `<br><small>${snapshot.ram_slots_used || '?'} / ${snapshot.ram_slots_total || '?'} slots</small>`;
|
|
}
|
|
|
|
const hardwareItems = [
|
|
{ label: 'CPU', icon: '🔲', value: `${snapshot.cpu_model || 'N/A'}<br><small>${snapshot.cpu_cores || 0}C / ${snapshot.cpu_threads || 0}T @ ${snapshot.cpu_max_freq_ghz || snapshot.cpu_base_freq_ghz || '?'} GHz</small>` },
|
|
{ label: 'RAM', icon: '💾', value: ramValue },
|
|
{ label: 'GPU', icon: '🎮', value: snapshot.gpu_model || snapshot.gpu_summary || 'N/A' },
|
|
{ label: 'Stockage', icon: '💿', value: snapshot.storage_summary || 'N/A' },
|
|
{ label: 'Réseau', icon: '🌐', value: snapshot.network_interfaces_json ? `${JSON.parse(snapshot.network_interfaces_json).length} interface(s)` : 'N/A' },
|
|
{ label: 'Carte mère', icon: '⚡', value: `${snapshot.motherboard_vendor || ''} ${snapshot.motherboard_model || 'N/A'}` },
|
|
{ label: 'OS', icon: '🐧', value: `${snapshot.os_name || 'N/A'} ${snapshot.os_version || ''}<br><small>Kernel ${snapshot.kernel_version || 'N/A'}</small>` },
|
|
{ label: 'Architecture', icon: '🏗️', value: snapshot.architecture || 'N/A' },
|
|
{ label: 'Virtualisation', icon: '📦', value: snapshot.virtualization_type || 'none' }
|
|
];
|
|
|
|
document.getElementById('hardwareSummary').innerHTML = hardwareItems.map(item => `
|
|
<div class="hardware-item">
|
|
<div class="hardware-item-label">${item.icon} ${item.label}</div>
|
|
<div class="hardware-item-value">${item.value}</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// Render last benchmark scores
|
|
function renderLastBenchmark() {
|
|
const bench = currentDevice.last_benchmark;
|
|
|
|
if (!bench) {
|
|
document.getElementById('lastBenchmark').innerHTML =
|
|
'<p style="color: var(--text-muted);">Aucun benchmark disponible</p>';
|
|
return;
|
|
}
|
|
|
|
document.getElementById('lastBenchmark').innerHTML = `
|
|
<div style="margin-bottom: 1rem;">
|
|
<span style="color: var(--text-secondary);">Date: </span>
|
|
<strong>${formatDate(bench.run_at)}</strong>
|
|
<span style="margin-left: 1rem; color: var(--text-secondary);">Version: </span>
|
|
<strong>${escapeHtml(bench.bench_script_version || 'N/A')}</strong>
|
|
</div>
|
|
|
|
<div class="score-grid">
|
|
${createScoreBadge(bench.global_score, 'Global')}
|
|
${createScoreBadge(bench.cpu_score, 'CPU')}
|
|
${createScoreBadge(bench.memory_score, 'Mémoire')}
|
|
${createScoreBadge(bench.disk_score, 'Disque')}
|
|
${createScoreBadge(bench.network_score, 'Réseau')}
|
|
${createScoreBadge(bench.gpu_score, 'GPU')}
|
|
</div>
|
|
|
|
<div style="margin-top: 1rem;">
|
|
<button class="btn btn-secondary btn-sm" onclick="viewBenchmarkDetails(${bench.id})">
|
|
Voir les détails complets (JSON)
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Render network details
|
|
function renderNetworkDetails() {
|
|
const container = document.getElementById('networkDetails');
|
|
const snapshot = currentDevice.last_hardware_snapshot;
|
|
const bench = currentDevice.last_benchmark;
|
|
|
|
if (!snapshot || !snapshot.network_interfaces_json) {
|
|
container.innerHTML = '<p style="color: var(--text-muted);">Aucune information réseau disponible</p>';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const interfaces = JSON.parse(snapshot.network_interfaces_json);
|
|
|
|
if (!interfaces || interfaces.length === 0) {
|
|
container.innerHTML = '<p style="color: var(--text-muted);">Aucune interface réseau détectée</p>';
|
|
return;
|
|
}
|
|
|
|
let html = '<div style="display: grid; gap: 1rem;">';
|
|
|
|
// Interface details
|
|
interfaces.forEach(iface => {
|
|
const typeIcon = iface.type === 'ethernet' ? '🔌' : (iface.type === 'wifi' ? '📡' : '🌐');
|
|
const wol = iface.wake_on_lan;
|
|
const wolBadge = wol === true
|
|
? '<span class="badge badge-success" style="margin-left: 0.5rem;">WoL ✓</span>'
|
|
: (wol === false ? '<span class="badge badge-muted" style="margin-left: 0.5rem;">WoL ✗</span>' : '');
|
|
|
|
html += `
|
|
<div class="hardware-item" style="border: 1px solid var(--border-color); border-radius: 8px; padding: 1rem;">
|
|
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 0.75rem;">
|
|
<div>
|
|
<div style="font-weight: 600; color: var(--color-primary);">${typeIcon} ${escapeHtml(iface.name)}</div>
|
|
<div style="color: var(--text-secondary); font-size: 0.9rem; margin-top: 0.25rem;">${escapeHtml(iface.type || 'unknown')}</div>
|
|
</div>
|
|
<div>${wolBadge}</div>
|
|
</div>
|
|
|
|
<div style="display: grid; gap: 0.5rem; font-size: 0.9rem;">
|
|
${iface.ip ? `<div><strong>IP:</strong> ${escapeHtml(iface.ip)}</div>` : ''}
|
|
${iface.mac ? `<div><strong>MAC:</strong> <code>${escapeHtml(iface.mac)}</code></div>` : ''}
|
|
${iface.speed_mbps ? `<div><strong>Vitesse:</strong> ${iface.speed_mbps} Mbps</div>` : ''}
|
|
${iface.driver ? `<div><strong>Driver:</strong> ${escapeHtml(iface.driver)}</div>` : ''}
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
// Network benchmark results (iperf3)
|
|
if (bench && bench.network_score !== null && bench.network_score !== undefined) {
|
|
let netBenchHtml = '<div style="border: 2px solid var(--color-info); border-radius: 8px; padding: 1rem; margin-top: 1rem;">';
|
|
netBenchHtml += '<div style="font-weight: 600; color: var(--color-info); margin-bottom: 0.75rem;">📈 Résultats Benchmark Réseau (iperf3)</div>';
|
|
netBenchHtml += '<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem;">';
|
|
|
|
// Try to parse network_results_json if available
|
|
let uploadMbps = null;
|
|
let downloadMbps = null;
|
|
let pingMs = null;
|
|
|
|
if (bench.network_results_json) {
|
|
try {
|
|
const netResults = typeof bench.network_results_json === 'string'
|
|
? JSON.parse(bench.network_results_json)
|
|
: bench.network_results_json;
|
|
uploadMbps = netResults.upload_mbps;
|
|
downloadMbps = netResults.download_mbps;
|
|
pingMs = netResults.ping_ms;
|
|
} catch (e) {
|
|
console.warn('Failed to parse network_results_json:', e);
|
|
}
|
|
}
|
|
|
|
if (uploadMbps !== null && uploadMbps !== undefined) {
|
|
netBenchHtml += `
|
|
<div style="text-align: center;">
|
|
<div style="font-size: 1.5rem; font-weight: 600; color: var(--color-success);">↑ ${uploadMbps.toFixed(2)}</div>
|
|
<div style="color: var(--text-secondary); font-size: 0.85rem;">Upload Mbps</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (downloadMbps !== null && downloadMbps !== undefined) {
|
|
netBenchHtml += `
|
|
<div style="text-align: center;">
|
|
<div style="font-size: 1.5rem; font-weight: 600; color: var(--color-info);">↓ ${downloadMbps.toFixed(2)}</div>
|
|
<div style="color: var(--text-secondary); font-size: 0.85rem;">Download Mbps</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (pingMs !== null && pingMs !== undefined) {
|
|
netBenchHtml += `
|
|
<div style="text-align: center;">
|
|
<div style="font-size: 1.5rem; font-weight: 600; color: var(--color-warning);">${typeof pingMs === 'number' ? pingMs.toFixed(2) : pingMs}</div>
|
|
<div style="color: var(--text-secondary); font-size: 0.85rem;">Ping ms</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
netBenchHtml += `
|
|
<div style="text-align: center;">
|
|
<div style="font-size: 1.5rem; font-weight: 600; color: var(--color-primary);">${bench.network_score.toFixed(2)}</div>
|
|
<div style="color: var(--text-secondary); font-size: 0.85rem;">Score</div>
|
|
</div>
|
|
`;
|
|
|
|
netBenchHtml += '</div></div>';
|
|
html += netBenchHtml;
|
|
}
|
|
|
|
html += '</div>';
|
|
container.innerHTML = html;
|
|
|
|
} catch (error) {
|
|
console.error('Failed to parse network interfaces:', error);
|
|
container.innerHTML = '<p style="color: var(--text-danger);">Erreur lors du parsing des données réseau</p>';
|
|
}
|
|
}
|
|
|
|
// Load benchmark history
|
|
async function loadBenchmarkHistory() {
|
|
const container = document.getElementById('benchmarkHistory');
|
|
|
|
try {
|
|
const data = await api.getDeviceBenchmarks(currentDeviceId, { limit: 20 });
|
|
|
|
if (!data.items || data.items.length === 0) {
|
|
showEmptyState(container, 'Aucun benchmark dans l\'historique', '📊');
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = `
|
|
<div class="table-wrapper">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Date</th>
|
|
<th>Score Global</th>
|
|
<th>CPU</th>
|
|
<th>MEM</th>
|
|
<th>DISK</th>
|
|
<th>NET</th>
|
|
<th>GPU</th>
|
|
<th>Version</th>
|
|
<th>Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${data.items.map(bench => `
|
|
<tr>
|
|
<td>${formatDate(bench.run_at)}</td>
|
|
<td><span class="${window.BenchUtils.getScoreBadgeClass(bench.global_score)}">${getScoreBadgeText(bench.global_score)}</span></td>
|
|
<td><span class="${window.BenchUtils.getScoreBadgeClass(bench.cpu_score)}">${getScoreBadgeText(bench.cpu_score)}</span></td>
|
|
<td><span class="${window.BenchUtils.getScoreBadgeClass(bench.memory_score)}">${getScoreBadgeText(bench.memory_score)}</span></td>
|
|
<td><span class="${window.BenchUtils.getScoreBadgeClass(bench.disk_score)}">${getScoreBadgeText(bench.disk_score)}</span></td>
|
|
<td><span class="${window.BenchUtils.getScoreBadgeClass(bench.network_score)}">${getScoreBadgeText(bench.network_score)}</span></td>
|
|
<td><span class="${window.BenchUtils.getScoreBadgeClass(bench.gpu_score)}">${getScoreBadgeText(bench.gpu_score)}</span></td>
|
|
<td><small>${escapeHtml(bench.bench_script_version || 'N/A')}</small></td>
|
|
<td>
|
|
<button class="btn btn-sm btn-secondary" onclick="viewBenchmarkDetails(${bench.id})">Détails</button>
|
|
</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
`;
|
|
|
|
} catch (error) {
|
|
console.error('Failed to load benchmarks:', error);
|
|
showError(container, 'Erreur lors du chargement de l\'historique');
|
|
}
|
|
}
|
|
|
|
// View benchmark details
|
|
async function viewBenchmarkDetails(benchmarkId) {
|
|
const modalBody = document.getElementById('benchmarkModalBody');
|
|
openModal('benchmarkModal');
|
|
|
|
try {
|
|
const benchmark = await api.getBenchmark(benchmarkId);
|
|
|
|
modalBody.innerHTML = `
|
|
<div class="code-block" style="max-height: 500px; overflow-y: auto;">
|
|
<pre><code>${JSON.stringify(benchmark.details || benchmark, null, 2)}</code></pre>
|
|
</div>
|
|
`;
|
|
|
|
} catch (error) {
|
|
console.error('Failed to load benchmark details:', error);
|
|
modalBody.innerHTML = `<div class="error">Erreur: ${escapeHtml(error.message)}</div>`;
|
|
}
|
|
}
|
|
|
|
// Load documents
|
|
async function loadDocuments() {
|
|
const container = document.getElementById('documentsList');
|
|
|
|
try {
|
|
const docs = await api.getDeviceDocs(currentDeviceId);
|
|
|
|
if (!docs || docs.length === 0) {
|
|
showEmptyState(container, 'Aucun document uploadé', '📄');
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = `
|
|
<ul class="document-list">
|
|
${docs.map(doc => `
|
|
<li class="document-item">
|
|
<div class="document-info">
|
|
<span class="document-icon">${getDocIcon(doc.doc_type)}</span>
|
|
<div>
|
|
<div class="document-name">${escapeHtml(doc.filename)}</div>
|
|
<div class="document-meta">
|
|
${doc.doc_type} • ${formatFileSize(doc.size_bytes)} • ${formatDate(doc.uploaded_at)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="document-actions">
|
|
<a href="${api.getDocumentDownloadUrl(doc.id)}" class="btn btn-sm btn-secondary" download>Télécharger</a>
|
|
<button class="btn btn-sm btn-danger" onclick="deleteDocument(${doc.id})">Supprimer</button>
|
|
</div>
|
|
</li>
|
|
`).join('')}
|
|
</ul>
|
|
`;
|
|
|
|
} catch (error) {
|
|
console.error('Failed to load documents:', error);
|
|
showError(container, 'Erreur lors du chargement des documents');
|
|
}
|
|
}
|
|
|
|
// Get document icon
|
|
function getDocIcon(docType) {
|
|
const icons = {
|
|
manual: '📘',
|
|
warranty: '📜',
|
|
invoice: '🧾',
|
|
photo: '📷',
|
|
other: '📄'
|
|
};
|
|
return icons[docType] || '📄';
|
|
}
|
|
|
|
// Upload document
|
|
async function uploadDocument() {
|
|
const fileInput = document.getElementById('fileInput');
|
|
const docTypeSelect = document.getElementById('docTypeSelect');
|
|
|
|
if (!fileInput.files || fileInput.files.length === 0) {
|
|
showToast('Veuillez sélectionner un fichier', 'error');
|
|
return;
|
|
}
|
|
|
|
const file = fileInput.files[0];
|
|
const docType = docTypeSelect.value;
|
|
|
|
try {
|
|
await api.uploadDocument(currentDeviceId, file, docType);
|
|
showToast('Document uploadé avec succès', 'success');
|
|
|
|
// Reset form
|
|
fileInput.value = '';
|
|
docTypeSelect.value = 'manual';
|
|
|
|
// Reload documents
|
|
await loadDocuments();
|
|
|
|
} catch (error) {
|
|
console.error('Failed to upload document:', error);
|
|
showToast('Erreur lors de l\'upload: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
// Delete document
|
|
async function deleteDocument(docId) {
|
|
if (!confirm('Êtes-vous sûr de vouloir supprimer ce document ?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await api.deleteDocument(docId);
|
|
showToast('Document supprimé', 'success');
|
|
await loadDocuments();
|
|
|
|
} catch (error) {
|
|
console.error('Failed to delete document:', error);
|
|
showToast('Erreur lors de la suppression: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
// Load links
|
|
async function loadLinks() {
|
|
const container = document.getElementById('linksList');
|
|
|
|
try {
|
|
const links = await api.getDeviceLinks(currentDeviceId);
|
|
|
|
if (!links || links.length === 0) {
|
|
showEmptyState(container, 'Aucun lien ajouté', '🔗');
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = `
|
|
<ul class="link-list">
|
|
${links.map(link => `
|
|
<li class="link-item">
|
|
<div class="link-info">
|
|
<a href="${escapeHtml(link.url)}" target="_blank" rel="noopener noreferrer">
|
|
🔗 ${escapeHtml(link.label)}
|
|
</a>
|
|
<div class="link-label">${escapeHtml(link.url)}</div>
|
|
</div>
|
|
<div class="link-actions">
|
|
<button class="btn btn-sm btn-danger" onclick="deleteLink(${link.id})">Supprimer</button>
|
|
</div>
|
|
</li>
|
|
`).join('')}
|
|
</ul>
|
|
`;
|
|
|
|
} catch (error) {
|
|
console.error('Failed to load links:', error);
|
|
showError(container, 'Erreur lors du chargement des liens');
|
|
}
|
|
}
|
|
|
|
// Add link
|
|
async function addLink() {
|
|
const labelInput = document.getElementById('linkLabel');
|
|
const urlInput = document.getElementById('linkUrl');
|
|
|
|
const label = labelInput.value.trim();
|
|
const url = urlInput.value.trim();
|
|
|
|
if (!label || !url) {
|
|
showToast('Veuillez remplir tous les champs', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await api.addDeviceLink(currentDeviceId, { label, url });
|
|
showToast('Lien ajouté avec succès', 'success');
|
|
|
|
// Reset form
|
|
labelInput.value = '';
|
|
urlInput.value = '';
|
|
|
|
// Reload links
|
|
await loadLinks();
|
|
|
|
} catch (error) {
|
|
console.error('Failed to add link:', error);
|
|
showToast('Erreur lors de l\'ajout: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
// Delete link
|
|
async function deleteLink(linkId) {
|
|
if (!confirm('Êtes-vous sûr de vouloir supprimer ce lien ?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await api.deleteLink(linkId);
|
|
showToast('Lien supprimé', 'success');
|
|
await loadLinks();
|
|
|
|
} catch (error) {
|
|
console.error('Failed to delete link:', error);
|
|
showToast('Erreur lors de la suppression: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
// Make functions available globally
|
|
window.viewBenchmarkDetails = viewBenchmarkDetails;
|
|
window.uploadDocument = uploadDocument;
|
|
window.deleteDocument = deleteDocument;
|
|
window.addLink = addLink;
|
|
window.deleteLink = deleteLink;
|