diff --git a/README.md b/README.md index a87ac91..e02fc10 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,11 @@ A lightweight web interface for meshcore-cli, providing browser-based access to - ๐ **Public channels** - Join public channels (starting with #) without encryption keys - ๐ฏ **Reply to users** - Quick reply with `@[UserName]` format - ๐ฅ **Contact management** - Manual contact approval mode with pending contacts list (persistent settings) -- ๐งน **Clean contacts** - Remove inactive contacts with configurable threshold + - **Dedicated pages:** Separate full-screen views for pending and existing contacts + - **Advanced sorting:** Sort contacts by name (A-Z/Z-A) or last advertisement time (newest/oldest) + - **Smart filtering:** Search by name/key, filter by contact type (CLI, REP, ROOM, SENS) + - **Activity indicators:** Visual status icons (๐ข active, ๐ก recent, ๐ด inactive) based on last advertisement + - **Cleanup tool:** Remove inactive contacts with configurable threshold (moved from Settings) - ๐ฆ **Message archiving** - Automatic daily archiving with browse-by-date selector - โก **Efficient polling** - Lightweight update checks every 10s, UI refreshes only when needed - ๐ก **Network commands** - Send advertisement (advert) or flood advertisement (floodadv) for network management @@ -164,11 +168,16 @@ mc-webui/ โ โ โ โโโ style.css # Custom styles โ โ โโโ js/ โ โ โโโ app.js # Main page frontend logic -โ โ โโโ dm.js # Direct Messages page logic +โ โ โโโ dm.js # Direct Messages page logic +โ โ โโโ contacts.js # Contact Management multi-page logic โ โโโ templates/ โ โโโ base.html # Base template โ โโโ index.html # Main chat view โ โโโ dm.html # Direct Messages full-page view +โ โโโ contacts_base.html # Contact pages base template +โ โโโ contacts-manage.html # Contact Management settings & navigation +โ โโโ contacts-pending.html # Pending contacts full-screen view +โ โโโ contacts-existing.html # Existing contacts with sort/filter โ โโโ components/ # Reusable components โโโ requirements.txt # Python dependencies โโโ .env.example # Example environment config @@ -193,6 +202,7 @@ mc-webui/ - [x] Message Archiving (Daily archiving with browse-by-date selector) - [x] Smart Notifications (Unread counters per channel and total) - [x] Direct Messages (DM) - Private messaging with delivery status tracking +- [x] Advanced Contact Management - Multi-page interface with sorting, filtering, and activity tracking ### Next Steps diff --git a/app/meshcore/cli.py b/app/meshcore/cli.py index 7adea79..1b984e9 100644 --- a/app/meshcore/cli.py +++ b/app/meshcore/cli.py @@ -628,11 +628,15 @@ def delete_contact(selector: str) -> Tuple[bool, str]: try: success, stdout, stderr = _run_command(['remove_contact', selector.strip()]) + # Log the meshcli response for debugging + logger.info(f"remove_contact {selector}: success={success}, stdout='{stdout}', stderr='{stderr}'") + if success: message = stdout.strip() if stdout.strip() else f"Contact {selector} removed successfully" return True, message else: error = stderr.strip() if stderr.strip() else "Failed to remove contact" + logger.warning(f"remove_contact failed for {selector}: {error}") return False, error except Exception as e: diff --git a/app/routes/api.py b/app/routes/api.py index e783f34..883bd4c 100644 --- a/app/routes/api.py +++ b/app/routes/api.py @@ -1209,6 +1209,8 @@ def get_contacts_detailed_api(): """ Get detailed list of ALL existing contacts on the device (CLI, REP, ROOM, SENS). + Returns full contact_info data from meshcli including GPS coordinates, paths, etc. + Returns: JSON with contacts list: { @@ -1217,53 +1219,75 @@ def get_contacts_detailed_api(): "limit": 350, "contacts": [ { - "name": "TK Zalesie Test ๐ฆ", - "public_key_prefix": "df2027d3f2ef", - "type_label": "REP", - "path_or_mode": "Flood", - "last_seen": 1735429453, // Unix timestamp from last_advert - "raw_line": "..." + "name": "TK Zalesie Test ๐ฆ", // adv_name + "public_key": "df2027d3f2ef...", // Full public key (64 chars) + "public_key_prefix": "df2027d3f2ef", // First 12 chars + "type": 2, // 1=CLI, 2=REP, 3=ROOM, 4=SENS + "type_label": "REP", // Human-readable type + "flags": 0, + "out_path_len": -1, // -1 = Flood mode + "out_path": "", // Path string + "last_advert": 1735429453, // Unix timestamp + "adv_lat": 50.866005, // GPS latitude + "adv_lon": 20.669308, // GPS longitude + "lastmod": 1715973527 // Last modification timestamp }, ... ] } """ try: - # Get basic contacts list - success, contacts, total_count, error = cli.get_all_contacts_detailed() + # Get detailed contact info from meshcli (includes all fields) + success_detailed, contacts_detailed, error_detailed = cli.get_contacts_with_last_seen() - if not success: + if not success_detailed: return jsonify({ 'success': False, - 'error': error or 'Failed to get contacts list', + 'error': error_detailed or 'Failed to get contact details', 'contacts': [], 'count': 0, 'limit': 350 }), 500 - # Get detailed contact info with last_advert timestamps - success_detailed, contacts_detailed, error_detailed = cli.get_contacts_with_last_seen() + # Convert dict to list and add computed fields + type_labels = {1: 'CLI', 2: 'REP', 3: 'ROOM', 4: 'SENS'} + contacts = [] - if success_detailed: - # Merge last_advert data with contacts - # Match by public_key_prefix (first 12 chars of full public_key) - for contact in contacts: - prefix = contact.get('public_key_prefix', '').lower() + for public_key, details in contacts_detailed.items(): + # Compute path display string + out_path_len = details.get('out_path_len', -1) + out_path = details.get('out_path', '') + if out_path_len == -1: + path_or_mode = 'Flood' + elif out_path: + path_or_mode = out_path + else: + path_or_mode = f'Path len: {out_path_len}' - # Find matching contact in detailed data - for full_key, details in contacts_detailed.items(): - if full_key.lower().startswith(prefix): - # Add last_seen timestamp - contact['last_seen'] = details.get('last_advert', None) - break - else: - # If detailed fetch failed, log warning but still return contacts without last_seen - logger.warning(f"Failed to get last_seen data: {error_detailed}") + contact = { + # All original fields from contact_info + 'public_key': public_key, + 'type': details.get('type'), + 'flags': details.get('flags'), + 'out_path_len': out_path_len, + 'out_path': out_path, + 'last_advert': details.get('last_advert'), + 'adv_lat': details.get('adv_lat'), + 'adv_lon': details.get('adv_lon'), + 'lastmod': details.get('lastmod'), + # Computed/convenience fields + 'name': details.get('adv_name', ''), # Map adv_name to name for compatibility + 'public_key_prefix': public_key[:12] if len(public_key) >= 12 else public_key, + 'type_label': type_labels.get(details.get('type'), 'UNKNOWN'), + 'path_or_mode': path_or_mode, # For UI display + 'last_seen': details.get('last_advert'), # Alias for compatibility + } + contacts.append(contact) return jsonify({ 'success': True, 'contacts': contacts, - 'count': total_count, + 'count': len(contacts), 'limit': 350 # MeshCore device limit }), 200 diff --git a/app/routes/views.py b/app/routes/views.py index e72373e..1abebec 100644 --- a/app/routes/views.py +++ b/app/routes/views.py @@ -44,10 +44,34 @@ def direct_messages(): @views_bp.route('/contacts/manage') def contact_management(): """ - Contact Management view - manual approval settings and pending contacts list. + Contact Management Settings - manual approval + cleanup + navigation. """ return render_template( - 'contacts.html', + 'contacts-manage.html', + device_name=config.MC_DEVICE_NAME, + refresh_interval=config.MC_REFRESH_INTERVAL + ) + + +@views_bp.route('/contacts/pending') +def contact_pending_list(): + """ + Full-screen pending contacts list. + """ + return render_template( + 'contacts-pending.html', + device_name=config.MC_DEVICE_NAME, + refresh_interval=config.MC_REFRESH_INTERVAL + ) + + +@views_bp.route('/contacts/existing') +def contact_existing_list(): + """ + Full-screen existing contacts list with search, filter, sort. + """ + return render_template( + 'contacts-existing.html', device_name=config.MC_DEVICE_NAME, refresh_interval=config.MC_REFRESH_INTERVAL ) diff --git a/app/static/js/app.js b/app/static/js/app.js index 925816c..e2367b2 100644 --- a/app/static/js/app.js +++ b/app/static/js/app.js @@ -99,10 +99,13 @@ function setupEventListeners() { } }); - // Cleanup contacts button - document.getElementById('cleanupBtn').addEventListener('click', function() { - cleanupContacts(); - }); + // Cleanup contacts button (only exists on contact management page) + const cleanupBtn = document.getElementById('cleanupBtn'); + if (cleanupBtn) { + cleanupBtn.addEventListener('click', function() { + cleanupContacts(); + }); + } // Track user scrolling const container = document.getElementById('messagesContainer'); diff --git a/app/static/js/contacts.js b/app/static/js/contacts.js index d0fbf51..bb7f632 100644 --- a/app/static/js/contacts.js +++ b/app/static/js/contacts.js @@ -1,10 +1,11 @@ /** - * Contact Management UI + * Contact Management UI - Multi-Page Version * * Features: * - Manual contact approval toggle (persistent across restarts) * - Pending contacts list with approve/copy actions - * - Existing contacts list with search, filter, and delete + * - Existing contacts list with search, filter, and sort + * - Three dedicated pages: manage, pending, existing * - Auto-refresh on page load * - Mobile-first design */ @@ -13,12 +14,17 @@ // State Management // ============================================================================= +let currentPage = null; // 'manage', 'pending', 'existing' let manualApprovalEnabled = false; let pendingContacts = []; let existingContacts = []; let filteredContacts = []; let contactToDelete = null; +// Sort state (for existing page) +let sortBy = 'last_advert'; // 'name' or 'last_advert' +let sortOrder = 'desc'; // 'asc' or 'desc' + // ============================================================================= // Initialization // ============================================================================= @@ -32,34 +38,209 @@ document.addEventListener('DOMContentLoaded', () => { return new bootstrap.Tooltip(tooltipTriggerEl); }); - // Attach event listeners - attachEventListeners(); + // Detect current page + detectCurrentPage(); - // Load initial state - loadSettings(); - loadPendingContacts(); - loadExistingContacts(); + // Initialize page-specific functionality + initializePage(); }); -function attachEventListeners() { +function detectCurrentPage() { + if (document.getElementById('managePageContent')) { + currentPage = 'manage'; + } else if (document.getElementById('pendingPageContent')) { + currentPage = 'pending'; + } else if (document.getElementById('existingPageContent')) { + currentPage = 'existing'; + } + console.log('Current page:', currentPage); +} + +function initializePage() { + switch (currentPage) { + case 'manage': + initManagePage(); + break; + case 'pending': + initPendingPage(); + break; + case 'existing': + initExistingPage(); + break; + default: + console.warn('Unknown page type'); + } +} + +// ============================================================================= +// Management Page Initialization +// ============================================================================= + +function initManagePage() { + console.log('Initializing Management page...'); + + // Load settings for manual approval toggle + loadSettings(); + + // Load contact counts for badges + loadContactCounts(); + + // Attach event listeners for manage page + attachManageEventListeners(); +} + +function attachManageEventListeners() { // Manual approval toggle const approvalSwitch = document.getElementById('manualApprovalSwitch'); if (approvalSwitch) { approvalSwitch.addEventListener('change', handleApprovalToggle); } - // Pending contacts refresh button - const refreshPendingBtn = document.getElementById('refreshPendingBtn'); - if (refreshPendingBtn) { - refreshPendingBtn.addEventListener('click', () => { + // Cleanup button + const cleanupBtn = document.getElementById('cleanupBtn'); + if (cleanupBtn) { + cleanupBtn.addEventListener('click', handleCleanupInactive); + } +} + +async function loadContactCounts() { + try { + // Fetch pending count + const pendingResp = await fetch('/api/contacts/pending'); + const pendingData = await pendingResp.json(); + + const pendingBadge = document.getElementById('pendingBadge'); + if (pendingBadge && pendingData.success) { + const count = pendingData.pending?.length || 0; + pendingBadge.textContent = count; + pendingBadge.classList.remove('spinner-border', 'spinner-border-sm'); + } + + // Fetch existing count + const existingResp = await fetch('/api/contacts/detailed'); + const existingData = await existingResp.json(); + + const existingBadge = document.getElementById('existingBadge'); + if (existingBadge && existingData.success) { + const count = existingData.count || 0; + const limit = existingData.limit || 350; + existingBadge.textContent = `${count} / ${limit}`; + existingBadge.classList.remove('spinner-border', 'spinner-border-sm'); + + // Apply counter color coding + existingBadge.classList.remove('counter-ok', 'counter-warning', 'counter-alarm'); + if (count >= 340) { + existingBadge.classList.add('counter-alarm'); + } else if (count >= 300) { + existingBadge.classList.add('counter-warning'); + } else { + existingBadge.classList.add('counter-ok'); + } + } + } catch (error) { + console.error('Error loading contact counts:', error); + } +} + +async function handleCleanupInactive() { + const hoursInput = document.getElementById('inactiveHours'); + const cleanupBtn = document.getElementById('cleanupBtn'); + + if (!hoursInput || !cleanupBtn) return; + + const hours = parseInt(hoursInput.value); + + if (isNaN(hours) || hours < 1) { + showToast('Please enter a valid number of hours', 'warning'); + return; + } + + // Confirm action + if (!confirm(`This will remove all contacts inactive for more than ${hours} hours. Continue?`)) { + return; + } + + // Disable button during operation + const originalHTML = cleanupBtn.innerHTML; + cleanupBtn.disabled = true; + cleanupBtn.innerHTML = ' Cleaning...'; + + try { + const response = await fetch('/api/contacts/cleanup', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + inactive_hours: hours + }) + }); + + const data = await response.json(); + + if (data.success) { + showToast(data.message || 'Cleanup completed successfully', 'success'); + // Reload contact counts + loadContactCounts(); + } else { + showToast('Cleanup failed: ' + (data.error || 'Unknown error'), 'danger'); + } + } catch (error) { + console.error('Error during cleanup:', error); + showToast('Network error during cleanup', 'danger'); + } finally { + // Re-enable button + cleanupBtn.disabled = false; + cleanupBtn.innerHTML = originalHTML; + } +} + +// ============================================================================= +// Pending Page Initialization +// ============================================================================= + +function initPendingPage() { + console.log('Initializing Pending page...'); + + // Load pending contacts + loadPendingContacts(); + + // Attach event listeners for pending page + attachPendingEventListeners(); +} + +function attachPendingEventListeners() { + // Refresh button + const refreshBtn = document.getElementById('refreshPendingBtn'); + if (refreshBtn) { + refreshBtn.addEventListener('click', () => { loadPendingContacts(); }); } +} - // Existing contacts refresh button - const refreshExistingBtn = document.getElementById('refreshExistingBtn'); - if (refreshExistingBtn) { - refreshExistingBtn.addEventListener('click', () => { +// ============================================================================= +// Existing Page Initialization +// ============================================================================= + +function initExistingPage() { + console.log('Initializing Existing page...'); + + // Parse sort parameters from URL + parseSortParamsFromURL(); + + // Load existing contacts + loadExistingContacts(); + + // Attach event listeners for existing page + attachExistingEventListeners(); +} + +function attachExistingEventListeners() { + // Refresh button + const refreshBtn = document.getElementById('refreshExistingBtn'); + if (refreshBtn) { + refreshBtn.addEventListener('click', () => { loadExistingContacts(); }); } @@ -68,7 +249,7 @@ function attachEventListeners() { const searchInput = document.getElementById('searchInput'); if (searchInput) { searchInput.addEventListener('input', () => { - applyFilters(); + applySortAndFilters(); }); } @@ -76,7 +257,22 @@ function attachEventListeners() { const typeFilter = document.getElementById('typeFilter'); if (typeFilter) { typeFilter.addEventListener('change', () => { - applyFilters(); + applySortAndFilters(); + }); + } + + // Sort buttons + const sortByName = document.getElementById('sortByName'); + if (sortByName) { + sortByName.addEventListener('click', () => { + handleSortChange('name'); + }); + } + + const sortByLastAdvert = document.getElementById('sortByLastAdvert'); + if (sortByLastAdvert) { + sortByLastAdvert.addEventListener('click', () => { + handleSortChange('last_advert'); }); } @@ -90,7 +286,7 @@ function attachEventListeners() { } // ============================================================================= -// Settings Management +// Settings Management (shared) // ============================================================================= async function loadSettings() { @@ -134,9 +330,6 @@ async function handleApprovalToggle(event) { enabled ? 'Manual approval enabled' : 'Manual approval disabled', 'success' ); - - // Reload pending contacts after toggle - setTimeout(() => loadPendingContacts(), 500); } else { console.error('Failed to update setting:', data.error); showToast('Failed to update setting: ' + data.error, 'danger'); @@ -156,7 +349,6 @@ async function handleApprovalToggle(event) { function updateApprovalUI(enabled) { const switchEl = document.getElementById('manualApprovalSwitch'); const labelEl = document.getElementById('switchLabel'); - const infoEl = document.getElementById('approvalInfo'); if (switchEl) { switchEl.checked = enabled; @@ -167,10 +359,6 @@ function updateApprovalUI(enabled) { ? 'Manual approval enabled' : 'Automatic approval (default)'; } - - if (infoEl) { - infoEl.style.display = enabled ? 'none' : 'inline-block'; - } } // ============================================================================= @@ -182,7 +370,7 @@ async function loadPendingContacts() { const emptyEl = document.getElementById('pendingEmpty'); const listEl = document.getElementById('pendingList'); const errorEl = document.getElementById('pendingError'); - const countBadge = document.getElementById('pendingCount'); + const countBadge = document.getElementById('pendingCountBadge'); // Show loading state if (loadingEl) loadingEl.style.display = 'block'; @@ -207,7 +395,7 @@ async function loadPendingContacts() { // Render pending contacts list renderPendingList(pendingContacts); - // Update count badge + // Update count badge (in navbar) if (countBadge) { countBadge.textContent = pendingContacts.length; countBadge.style.display = 'inline-block'; @@ -216,7 +404,7 @@ async function loadPendingContacts() { } else { console.error('Failed to load pending contacts:', data.error); if (errorEl) { - const errorMsg = document.getElementById('errorMessage'); + const errorMsg = document.getElementById('pendingErrorMessage'); if (errorMsg) errorMsg.textContent = data.error || 'Failed to load pending contacts'; errorEl.style.display = 'block'; } @@ -225,7 +413,7 @@ async function loadPendingContacts() { console.error('Error loading pending contacts:', error); if (loadingEl) loadingEl.style.display = 'none'; if (errorEl) { - const errorMsg = document.getElementById('errorMessage'); + const errorMsg = document.getElementById('pendingErrorMessage'); if (errorMsg) errorMsg.textContent = 'Network error: ' + error.message; errorEl.style.display = 'block'; } @@ -425,15 +613,15 @@ async function loadExistingContacts() { existingContacts = data.contacts || []; filteredContacts = [...existingContacts]; - // Update counter badge + // Update counter badge (in navbar) updateCounter(data.count, data.limit); if (existingContacts.length === 0) { // Show empty state if (emptyEl) emptyEl.style.display = 'block'; } else { - // Apply filters and render - applyFilters(); + // Apply filters and sort + applySortAndFilters(); } } else { console.error('Failed to load existing contacts:', data.error); @@ -474,14 +662,83 @@ function updateCounter(count, limit) { } } -function applyFilters() { +// ============================================================================= +// 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 UI to reflect current sort + updateSortUI(); +} + +function handleSortChange(newSortBy) { + if (sortBy === newSortBy) { + // Toggle order + sortOrder = sortOrder === 'asc' ? 'desc' : 'asc'; + } else { + // Change sort field + sortBy = newSortBy; + // Set default order for new field + sortOrder = newSortBy === 'name' ? 'asc' : 'desc'; + } + + console.log('Sort changed to:', { sortBy, sortOrder }); + + // Update URL parameters + updateURLWithSortParams(); + + // Update UI + updateSortUI(); + + // Re-apply filters and sort + applySortAndFilters(); +} + +function updateURLWithSortParams() { + const url = new URL(window.location); + url.searchParams.set('sort', sortBy); + url.searchParams.set('order', sortOrder); + window.history.replaceState({}, '', url); +} + +function updateSortUI() { + // Update sort button active states and icons + const sortButtons = document.querySelectorAll('.sort-btn'); + + sortButtons.forEach(btn => { + const btnSort = btn.dataset.sort; + const icon = btn.querySelector('i'); + + if (btnSort === sortBy) { + // Active button + btn.classList.add('active'); + if (icon) { + icon.className = sortOrder === 'asc' ? 'bi bi-sort-up' : 'bi bi-sort-down'; + } + } else { + // Inactive button + btn.classList.remove('active'); + if (icon) { + icon.className = 'bi bi-sort-down'; // Default icon + } + } + }); +} + +function applySortAndFilters() { const searchInput = document.getElementById('searchInput'); const typeFilter = document.getElementById('typeFilter'); const searchTerm = searchInput ? searchInput.value.toLowerCase() : ''; const selectedType = typeFilter ? typeFilter.value : 'ALL'; - // Filter contacts + // First, filter contacts filteredContacts = existingContacts.filter(contact => { // Type filter if (selectedType !== 'ALL' && contact.type_label !== selectedType) { @@ -498,7 +755,20 @@ function applyFilters() { return true; }); - // Render filtered contacts + // 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); } @@ -569,7 +839,7 @@ function formatRelativeTime(timestamp) { } /** - * Get activity status indicator based on last_seen timestamp + * Get activity status indicator based on last_advert timestamp * Returns: { icon: string, color: string, title: string } */ function getActivityStatus(timestamp) { @@ -589,7 +859,7 @@ function getActivityStatus(timestamp) { return { icon: '๐ข', color: '#28a745', - title: 'Active (seen recently)' + title: 'Active (advert received recently)' }; } @@ -654,10 +924,10 @@ function createExistingContactCard(contact, index) { keyDiv.textContent = contact.public_key_prefix; keyDiv.title = 'Public Key Prefix'; - // Last seen row (with activity status indicator) - const lastSeenDiv = document.createElement('div'); - lastSeenDiv.className = 'text-muted small d-flex align-items-center gap-1'; - lastSeenDiv.style.marginBottom = '0.25rem'; + // 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); @@ -669,10 +939,10 @@ function createExistingContactCard(contact, index) { statusIcon.title = status.title; const timeText = document.createElement('span'); - timeText.textContent = `Last seen: ${relativeTime}`; + timeText.textContent = `Last advert: ${relativeTime}`; - lastSeenDiv.appendChild(statusIcon); - lastSeenDiv.appendChild(timeText); + lastAdvertDiv.appendChild(statusIcon); + lastAdvertDiv.appendChild(timeText); } else { // No last_seen data available const statusIcon = document.createElement('span'); @@ -680,10 +950,10 @@ function createExistingContactCard(contact, index) { statusIcon.style.fontSize = '0.9rem'; const timeText = document.createElement('span'); - timeText.textContent = 'Last seen: Unknown'; + timeText.textContent = 'Last advert: Unknown'; - lastSeenDiv.appendChild(statusIcon); - lastSeenDiv.appendChild(timeText); + lastAdvertDiv.appendChild(statusIcon); + lastAdvertDiv.appendChild(timeText); } // Path/mode (optional) @@ -716,7 +986,7 @@ function createExistingContactCard(contact, index) { // Assemble card card.appendChild(infoRow); card.appendChild(keyDiv); - card.appendChild(lastSeenDiv); + card.appendChild(lastAdvertDiv); if (pathDiv) card.appendChild(pathDiv); card.appendChild(actionsDiv); @@ -772,13 +1042,16 @@ async function confirmDelete() { } 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: contactToDelete.public_key_prefix // Use prefix for reliability + selector: selector }) }); diff --git a/app/templates/base.html b/app/templates/base.html index a2af338..cf328c4 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -244,19 +244,6 @@
Configure contact management preferences
++ + Approve or reject contacts waiting for manual approval. +
+