No contacts match filters
Try changing your filter criteria `; listEl.appendChild(emptyDiv); return; } contacts.forEach((contact, index) => { const card = createContactCard(contact, index); listEl.appendChild(card); }); } function createContactCard(contact, index) { const card = document.createElement('div'); card.className = 'pending-contact-card'; card.id = `contact-${index}`; // Contact info row (name + type badge) const infoRow = document.createElement('div'); infoRow.className = 'contact-info-row'; const nameDiv = document.createElement('div'); nameDiv.className = 'contact-name flex-grow-1'; nameDiv.textContent = contact.name; const typeBadge = document.createElement('span'); typeBadge.className = 'badge type-badge'; typeBadge.textContent = contact.type_label || 'CLI'; // Color-code by type (same as existing contacts) switch (contact.type_label) { case 'CLI': typeBadge.classList.add('bg-primary'); break; case 'REP': typeBadge.classList.add('bg-success'); break; case 'ROOM': typeBadge.classList.add('bg-info'); break; case 'SENS': typeBadge.classList.add('bg-warning', 'text-dark'); break; default: typeBadge.classList.add('bg-secondary'); } infoRow.appendChild(nameDiv); infoRow.appendChild(typeBadge); // Public key row (use prefix for display) const keyDiv = document.createElement('div'); keyDiv.className = 'contact-key'; keyDiv.textContent = contact.public_key_prefix || contact.public_key.substring(0, 12); keyDiv.title = 'Public Key Prefix'; // Last advert (optional - show if available) let lastAdvertDiv = null; if (contact.last_advert) { lastAdvertDiv = document.createElement('div'); lastAdvertDiv.className = 'text-muted small'; const relativeTime = formatRelativeTime(contact.last_advert); lastAdvertDiv.textContent = `Last seen: ${relativeTime}`; } // Action buttons const actionsDiv = document.createElement('div'); actionsDiv.className = 'd-flex gap-2 mt-2'; // Approve button const approveBtn = document.createElement('button'); approveBtn.className = 'btn btn-sm btn-success'; approveBtn.innerHTML = ' Approve'; approveBtn.onclick = () => approveContact(contact, index); actionsDiv.appendChild(approveBtn); // Map button (only if GPS coordinates available) if (contact.adv_lat && contact.adv_lon && (contact.adv_lat !== 0 || contact.adv_lon !== 0)) { const mapBtn = document.createElement('button'); mapBtn.className = 'btn btn-sm btn-outline-primary'; mapBtn.innerHTML = ' Map'; mapBtn.onclick = () => window.showContactOnMap(contact.name, contact.adv_lat, contact.adv_lon); actionsDiv.appendChild(mapBtn); } // Copy key button const copyBtn = document.createElement('button'); copyBtn.className = 'btn btn-sm btn-outline-secondary'; copyBtn.innerHTML = ' Copy Key'; copyBtn.onclick = () => copyPublicKey(contact.public_key, copyBtn); actionsDiv.appendChild(copyBtn); // Assemble card card.appendChild(infoRow); card.appendChild(keyDiv); if (lastAdvertDiv) card.appendChild(lastAdvertDiv); card.appendChild(actionsDiv); return card; } async function approveContact(contact, index) { const cardEl = document.getElementById(`contact-${index}`); // Disable buttons during approval if (cardEl) { const buttons = cardEl.querySelectorAll('button'); buttons.forEach(btn => btn.disabled = true); } try { const response = await fetch('/api/contacts/pending/approve', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ public_key: contact.public_key // ALWAYS use full public_key (works for CLI, ROOM, etc.) }) }); const data = await response.json(); if (data.success) { showToast(`Approved: ${contact.name}`, 'success'); // Remove from list with animation if (cardEl) { cardEl.style.opacity = '0'; cardEl.style.transition = 'opacity 0.3s'; setTimeout(() => { cardEl.remove(); // Reload pending list to update count loadPendingContacts(); }, 300); } } else { console.error('Failed to approve contact:', data.error); showToast('Failed to approve: ' + data.error, 'danger'); // Re-enable buttons if (cardEl) { const buttons = cardEl.querySelectorAll('button'); buttons.forEach(btn => btn.disabled = false); } } } catch (error) { console.error('Error approving contact:', error); showToast('Network error: ' + error.message, 'danger'); // Re-enable buttons if (cardEl) { const buttons = cardEl.querySelectorAll('button'); buttons.forEach(btn => btn.disabled = false); } } } function copyPublicKey(publicKey, buttonEl) { navigator.clipboard.writeText(publicKey).then(() => { // Visual feedback const originalHTML = buttonEl.innerHTML; buttonEl.innerHTML = ' Copied!'; buttonEl.classList.remove('btn-outline-secondary'); buttonEl.classList.add('btn-success'); setTimeout(() => { buttonEl.innerHTML = originalHTML; buttonEl.classList.remove('btn-success'); buttonEl.classList.add('btn-outline-secondary'); }, 2000); showToast('Public key copied to clipboard', 'info'); }).catch(err => { console.error('Failed to copy:', err); showToast('Failed to copy to clipboard', 'danger'); }); } // ============================================================================= // Pending Page - Filtering and Batch Approval // ============================================================================= function applyPendingFilters() { const searchInput = document.getElementById('pendingSearchInput'); const searchTerm = searchInput ? searchInput.value.toLowerCase() : ''; // Apply search filter locally (type filter already applied by API) filteredPendingContacts = pendingContacts.filter(contact => { // Search filter (name or public_key_prefix) if (searchTerm) { const nameMatch = contact.name.toLowerCase().includes(searchTerm); const keyMatch = (contact.public_key_prefix || contact.public_key).toLowerCase().includes(searchTerm); return nameMatch || keyMatch; } return true; }); // Update filtered count badge const countBadge = document.getElementById('filteredCountBadge'); if (countBadge) { countBadge.textContent = filteredPendingContacts.length; } // Render filtered list renderPendingList(filteredPendingContacts); } function showBatchApprovalModal() { if (filteredPendingContacts.length === 0) { showToast('No contacts to approve', 'warning'); return; } const modal = new bootstrap.Modal(document.getElementById('batchApprovalModal')); const countEl = document.getElementById('batchApprovalCount'); const listEl = document.getElementById('batchApprovalList'); // Update count if (countEl) countEl.textContent = filteredPendingContacts.length; // Populate list if (listEl) { listEl.innerHTML = ''; filteredPendingContacts.forEach(contact => { const item = document.createElement('div'); item.className = 'list-group-item d-flex justify-content-between align-items-center'; const nameSpan = document.createElement('span'); nameSpan.textContent = contact.name; const typeBadge = document.createElement('span'); typeBadge.className = 'badge'; typeBadge.textContent = contact.type_label; switch (contact.type_label) { case 'CLI': typeBadge.classList.add('bg-primary'); break; case 'REP': typeBadge.classList.add('bg-success'); break; case 'ROOM': typeBadge.classList.add('bg-info'); break; case 'SENS': typeBadge.classList.add('bg-warning', 'text-dark'); break; default: typeBadge.classList.add('bg-secondary'); } item.appendChild(nameSpan); item.appendChild(typeBadge); listEl.appendChild(item); }); } modal.show(); } async function batchApproveContacts() { const modal = bootstrap.Modal.getInstance(document.getElementById('batchApprovalModal')); const confirmBtn = document.getElementById('confirmBatchApprovalBtn'); if (confirmBtn) confirmBtn.disabled = true; let successCount = 0; let failedCount = 0; const failures = []; // Approve contacts one by one (sequential HTTP requests) for (let i = 0; i < filteredPendingContacts.length; i++) { const contact = filteredPendingContacts[i]; // Update button with progress if (confirmBtn) { confirmBtn.innerHTML = ` Approving ${i + 1}/${filteredPendingContacts.length}...`; } try { const response = await fetch('/api/contacts/pending/approve', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ public_key: contact.public_key }) }); const data = await response.json(); if (data.success) { successCount++; } else { failedCount++; failures.push({ name: contact.name, error: data.error }); } } catch (error) { failedCount++; failures.push({ name: contact.name, error: error.message }); } } // Close modal if (modal) modal.hide(); // Show result if (successCount > 0 && failedCount === 0) { showToast(`Successfully approved ${successCount} contact${successCount !== 1 ? 's' : ''}`, 'success'); } else if (successCount > 0 && failedCount > 0) { showToast(`Approved ${successCount}, failed ${failedCount}. Check console for details.`, 'warning'); console.error('Failed approvals:', failures); } else { showToast(`Failed to approve contacts. Check console for details.`, 'danger'); console.error('Failed approvals:', failures); } // Reload pending list loadPendingContacts(); // Re-enable button if (confirmBtn) { confirmBtn.disabled = false; confirmBtn.innerHTML = ' Approve All'; } } // ============================================================================= // Toast Notifications // ============================================================================= function showToast(message, type = 'info') { const toastEl = document.getElementById('contactToast'); if (!toastEl) return; const bodyEl = toastEl.querySelector('.toast-body'); if (!bodyEl) return; // Set message and style bodyEl.textContent = message; // Apply color based on type toastEl.classList.remove('bg-success', 'bg-danger', 'bg-info', 'bg-warning'); toastEl.classList.remove('text-white'); if (type === 'success' || type === 'danger' || type === 'warning') { toastEl.classList.add(`bg-${type}`, 'text-white'); } else if (type === 'info') { toastEl.classList.add('bg-info', 'text-white'); } // Show toast const toast = new bootstrap.Toast(toastEl, { autohide: true, delay: 1500 }); toast.show(); } // ============================================================================= // Existing Contacts Management // ============================================================================= async function loadExistingContacts() { const loadingEl = document.getElementById('existingLoading'); const emptyEl = document.getElementById('existingEmpty'); const listEl = document.getElementById('existingList'); const errorEl = document.getElementById('existingError'); // Show loading state if (loadingEl) loadingEl.style.display = 'block'; if (emptyEl) emptyEl.style.display = 'none'; if (listEl) listEl.innerHTML = ''; if (errorEl) errorEl.style.display = 'none'; try { // Fetch device contacts and cached contacts in parallel const [deviceResponse, cacheResponse] = await Promise.all([ fetch('/api/contacts/detailed'), fetch('/api/contacts/cached?format=full') ]); const deviceData = await deviceResponse.json(); const cacheData = await cacheResponse.json(); if (loadingEl) loadingEl.style.display = 'none'; if (deviceData.success) { const deviceContacts = deviceData.contacts || []; const cachedContacts = (cacheData.success && cacheData.contacts) ? cacheData.contacts : []; // Mark device contacts const deviceKeySet = new Set(deviceContacts.map(c => c.public_key)); deviceContacts.forEach(c => { c.on_device = true; }); // Add cache-only contacts (not on device) const cacheOnlyContacts = cachedContacts .filter(c => !deviceKeySet.has(c.public_key)) .map(c => ({ name: c.name || 'Unknown', public_key: c.public_key, public_key_prefix: c.public_key_prefix || c.public_key.substring(0, 12), type_label: c.type_label || '', adv_lat: c.lat || 0, adv_lon: c.lon || 0, last_seen: c.last_seen || 0, on_device: false, source: c.source || 'cache' })); existingContacts = [...deviceContacts, ...cacheOnlyContacts]; filteredContacts = [...existingContacts]; // Update counter badge updateCounter(deviceData.count, deviceData.limit, cachedContacts.length); if (existingContacts.length === 0) { if (emptyEl) emptyEl.style.display = 'block'; } else { applySortAndFilters(); } } else { console.error('Failed to load existing contacts:', deviceData.error); if (errorEl) { const errorMsg = document.getElementById('existingErrorMessage'); if (errorMsg) errorMsg.textContent = deviceData.error || 'Failed to load contacts'; errorEl.style.display = 'block'; } } } catch (error) { console.error('Error loading existing contacts:', error); if (loadingEl) loadingEl.style.display = 'none'; if (errorEl) { const errorMsg = document.getElementById('existingErrorMessage'); if (errorMsg) errorMsg.textContent = 'Network error: ' + error.message; errorEl.style.display = 'block'; } } } function updateCounter(count, limit, totalKnown) { const counterEl = document.getElementById('contactsCounter'); if (!counterEl) return; let text = `${count} / ${limit}`; if (totalKnown && totalKnown > count) { text += ` (${totalKnown} cached)`; } counterEl.textContent = text; counterEl.style.display = 'inline-block'; // Remove all counter classes counterEl.classList.remove('counter-ok', 'counter-warning', 'counter-alarm'); // Apply appropriate class based on count if (count >= 340) { counterEl.classList.add('counter-alarm'); } else if (count >= 300) { counterEl.classList.add('counter-warning'); } else { counterEl.classList.add('counter-ok'); } } // ============================================================================= // Sorting Functionality (Existing Page) // ============================================================================= function parseSortParamsFromURL() { const urlParams = new URLSearchParams(window.location.search); sortBy = urlParams.get('sort') || 'last_advert'; sortOrder = urlParams.get('order') || 'desc'; console.log('Parsed sort params:', { sortBy, sortOrder }); // Update sort dropdown to reflect current sort const sortSelect = document.getElementById('sortSelect'); if (sortSelect) { sortSelect.value = `${sortBy}_${sortOrder}`; } } function updateURLWithSortParams() { const url = new URL(window.location); url.searchParams.set('sort', sortBy); url.searchParams.set('order', sortOrder); window.history.replaceState({}, '', url); } function applySortAndFilters() { const searchInput = document.getElementById('searchInput'); const typeFilter = document.getElementById('typeFilter'); const sourceFilter = document.getElementById('sourceFilter'); const searchTerm = searchInput ? searchInput.value.toLowerCase() : ''; const selectedType = typeFilter ? typeFilter.value : 'ALL'; const selectedSource = sourceFilter ? sourceFilter.value : 'ALL'; // First, filter contacts filteredContacts = existingContacts.filter(contact => { // Source filter if (selectedSource === 'DEVICE' && !contact.on_device) return false; if (selectedSource === 'CACHE' && contact.on_device) return false; // Type filter (cache-only contacts have no type_label) if (selectedType !== 'ALL') { if (!contact.type_label || contact.type_label !== selectedType) { return false; } } // Search filter (name or public_key_prefix) if (searchTerm) { const nameMatch = contact.name.toLowerCase().includes(searchTerm); const keyMatch = (contact.public_key_prefix || '').toLowerCase().includes(searchTerm); return nameMatch || keyMatch; } return true; }); // Then, sort filtered contacts filteredContacts.sort((a, b) => { if (sortBy === 'name') { const comparison = a.name.localeCompare(b.name); return sortOrder === 'asc' ? comparison : -comparison; } else if (sortBy === 'last_advert') { const aTime = a.last_seen || 0; const bTime = b.last_seen || 0; return sortOrder === 'desc' ? bTime - aTime : aTime - bTime; } return 0; }); // Render sorted and filtered contacts renderExistingList(filteredContacts); } function renderExistingList(contacts) { const listEl = document.getElementById('existingList'); const emptyEl = document.getElementById('existingEmpty'); if (!listEl) return; listEl.innerHTML = ''; if (contacts.length === 0) { if (emptyEl) emptyEl.style.display = 'block'; return; } if (emptyEl) emptyEl.style.display = 'none'; contacts.forEach((contact, index) => { const card = createExistingContactCard(contact, index); listEl.appendChild(card); }); } /** * Format Unix timestamp as relative time ("5 minutes ago", "2 hours ago", etc.) */ function formatRelativeTime(timestamp) { if (!timestamp) return 'Never'; const now = Math.floor(Date.now() / 1000); // Current time in Unix seconds const diffSeconds = now - timestamp; if (diffSeconds < 0) return 'Just now'; // Future timestamp (clock skew) // Less than 1 minute if (diffSeconds < 60) { return 'Just now'; } // Less than 1 hour if (diffSeconds < 3600) { const minutes = Math.floor(diffSeconds / 60); return `${minutes} minute${minutes !== 1 ? 's' : ''} ago`; } // Less than 1 day if (diffSeconds < 86400) { const hours = Math.floor(diffSeconds / 3600); return `${hours} hour${hours !== 1 ? 's' : ''} ago`; } // Less than 30 days if (diffSeconds < 2592000) { const days = Math.floor(diffSeconds / 86400); return `${days} day${days !== 1 ? 's' : ''} ago`; } // Less than 1 year if (diffSeconds < 31536000) { const months = Math.floor(diffSeconds / 2592000); return `${months} month${months !== 1 ? 's' : ''} ago`; } // More than 1 year const years = Math.floor(diffSeconds / 31536000); return `${years} year${years !== 1 ? 's' : ''} ago`; } /** * Get activity status indicator based on last_advert timestamp * Returns: { icon: string, color: string, title: string } */ function getActivityStatus(timestamp) { if (!timestamp) { return { icon: '⚫', color: '#6c757d', title: 'Never seen' }; } const now = Math.floor(Date.now() / 1000); const diffSeconds = now - timestamp; // Active (< 5 minutes) if (diffSeconds < 300) { return { icon: '🟢', color: '#28a745', title: 'Active (advert received recently)' }; } // Recent (< 1 hour) if (diffSeconds < 3600) { return { icon: '🟡', color: '#ffc107', title: 'Recent activity' }; } // Inactive (> 1 hour) return { icon: '🔴', color: '#dc3545', title: 'Inactive' }; } function createExistingContactCard(contact, index) { const card = document.createElement('div'); card.className = 'existing-contact-card'; card.id = `existing-contact-${index}`; // Check if contact is protected const isProtected = contact.is_protected || isContactProtected(contact.public_key); // Contact info row (name + type badge) const infoRow = document.createElement('div'); infoRow.className = 'contact-info-row'; const nameDiv = document.createElement('div'); nameDiv.className = 'contact-name flex-grow-1'; nameDiv.textContent = contact.name; // Add protection indicator if protected if (isProtected) { const lockIndicator = document.createElement('span'); lockIndicator.className = 'protection-indicator ms-2'; lockIndicator.innerHTML = ''; nameDiv.appendChild(lockIndicator); } // Type badge - use type_label if available, fall back to "Cache" for unknown type const typeBadge = document.createElement('span'); typeBadge.className = 'badge type-badge'; if (contact.type_label) { typeBadge.textContent = contact.type_label; switch (contact.type_label) { case 'CLI': typeBadge.classList.add('bg-primary'); break; case 'REP': typeBadge.classList.add('bg-success'); break; case 'ROOM': typeBadge.classList.add('bg-info'); break; case 'SENS': typeBadge.classList.add('bg-warning'); break; default: typeBadge.classList.add('bg-secondary'); } } else { typeBadge.textContent = 'Cache'; typeBadge.classList.add('bg-secondary'); typeBadge.title = 'Not on device - type unknown'; } infoRow.appendChild(nameDiv); infoRow.appendChild(typeBadge); // Public key row (clickable to copy) const keyDiv = document.createElement('div'); keyDiv.className = 'contact-key clickable-key'; keyDiv.textContent = contact.public_key_prefix; keyDiv.title = 'Click to copy'; keyDiv.onclick = () => copyToClipboard(contact.public_key_prefix, keyDiv); // Last advert row (with activity status indicator) const lastAdvertDiv = document.createElement('div'); lastAdvertDiv.className = 'text-muted small d-flex align-items-center gap-1'; lastAdvertDiv.style.marginBottom = '0.25rem'; if (contact.last_seen) { const status = getActivityStatus(contact.last_seen); const relativeTime = formatRelativeTime(contact.last_seen); const statusIcon = document.createElement('span'); statusIcon.textContent = status.icon; statusIcon.style.fontSize = '0.9rem'; statusIcon.title = status.title; const timeText = document.createElement('span'); timeText.textContent = `Last advert: ${relativeTime}`; lastAdvertDiv.appendChild(statusIcon); lastAdvertDiv.appendChild(timeText); } else { // No last_seen data available const statusIcon = document.createElement('span'); statusIcon.textContent = '⚫'; statusIcon.style.fontSize = '0.9rem'; const timeText = document.createElement('span'); timeText.textContent = 'Last advert: Unknown'; lastAdvertDiv.appendChild(statusIcon); lastAdvertDiv.appendChild(timeText); } // Path/mode (optional) let pathDiv = null; if (contact.path_or_mode && contact.path_or_mode !== 'Flood') { pathDiv = document.createElement('div'); pathDiv.className = 'text-muted small'; pathDiv.textContent = `Path: ${contact.path_or_mode}`; } // Action buttons const actionsDiv = document.createElement('div'); actionsDiv.className = 'd-flex gap-2 mt-2'; // Map button - for ANY contact with GPS coordinates if (contact.adv_lat && contact.adv_lon && (contact.adv_lat !== 0 || contact.adv_lon !== 0)) { const mapBtn = document.createElement('button'); mapBtn.className = 'btn btn-sm btn-outline-primary'; mapBtn.innerHTML = ' Map'; mapBtn.onclick = () => window.showContactOnMap(contact.name, contact.adv_lat, contact.adv_lon); actionsDiv.appendChild(mapBtn); } // Protect & Delete buttons (only for device contacts) if (contact.on_device !== false) { const protectBtn = document.createElement('button'); protectBtn.className = isProtected ? 'btn btn-sm btn-warning' : 'btn btn-sm btn-outline-warning'; protectBtn.innerHTML = isProtected ? ' Protected' : ' Protect'; protectBtn.onclick = () => toggleContactProtection(contact.public_key, protectBtn); actionsDiv.appendChild(protectBtn); const deleteBtn = document.createElement('button'); deleteBtn.className = 'btn btn-sm btn-outline-danger'; deleteBtn.innerHTML = ' Delete'; deleteBtn.onclick = () => showDeleteModal(contact); deleteBtn.disabled = isProtected; if (isProtected) { deleteBtn.title = 'Cannot delete protected contact'; } actionsDiv.appendChild(deleteBtn); } // Assemble card card.appendChild(infoRow); card.appendChild(keyDiv); card.appendChild(lastAdvertDiv); if (pathDiv) card.appendChild(pathDiv); card.appendChild(actionsDiv); return card; } /** * Copy text to clipboard with fallback for HTTP contexts. * @param {string} text - Text to copy * @param {HTMLElement} element - Element for visual feedback */ function copyToClipboard(text, element) { const originalText = element.textContent; // Try modern clipboard API first (requires HTTPS) if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(() => { showCopyFeedback(element, originalText); }).catch(() => { // Fallback to legacy method legacyCopy(text, element, originalText); }); } else { // Fallback for HTTP contexts legacyCopy(text, element, originalText); } } /** * Legacy copy method using execCommand (works on HTTP). */ function legacyCopy(text, element, originalText) { const textArea = document.createElement('textarea'); textArea.value = text; textArea.style.position = 'fixed'; textArea.style.left = '-9999px'; document.body.appendChild(textArea); textArea.select(); try { document.execCommand('copy'); showCopyFeedback(element, originalText); } catch (err) { console.error('Failed to copy:', err); showToast('Failed to copy', 'danger'); } document.body.removeChild(textArea); } /** * Show visual feedback after successful copy. */ function showCopyFeedback(element, originalText) { element.textContent = 'Copied!'; element.classList.add('copied'); setTimeout(() => { element.textContent = originalText; element.classList.remove('copied'); }, 1500); showToast('Key copied to clipboard', 'info'); } function showDeleteModal(contact) { contactToDelete = contact; // Set modal content const modalNameEl = document.getElementById('deleteContactName'); const modalKeyEl = document.getElementById('deleteContactKey'); if (modalNameEl) modalNameEl.textContent = contact.name; if (modalKeyEl) modalKeyEl.textContent = contact.public_key_prefix; // Show modal const modal = new bootstrap.Modal(document.getElementById('deleteContactModal')); modal.show(); } async function confirmDelete() { if (!contactToDelete) return; const modal = bootstrap.Modal.getInstance(document.getElementById('deleteContactModal')); const confirmBtn = document.getElementById('confirmDeleteBtn'); // Disable button during deletion if (confirmBtn) { confirmBtn.disabled = true; confirmBtn.innerHTML = ' Deleting...'; } try { // Use contact name for deletion (meshcli remove_contact only works with name) const selector = contactToDelete.name; const response = await fetch('/api/contacts/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ selector: selector }) }); const data = await response.json(); if (data.success) { showToast(`Deleted: ${contactToDelete.name}`, 'success'); // Hide modal if (modal) modal.hide(); // Reload contacts list setTimeout(() => loadExistingContacts(), 500); } else { console.error('Failed to delete contact:', data.error); showToast('Failed to delete: ' + data.error, 'danger'); } } catch (error) { console.error('Error deleting contact:', error); showToast('Network error: ' + error.message, 'danger'); } finally { // Re-enable button if (confirmBtn) { confirmBtn.disabled = false; confirmBtn.innerHTML = ' Delete Contact'; } contactToDelete = null; } }