From 5f72f40742e4c42bef657860332ecfafeda81cef Mon Sep 17 00:00:00 2001 From: MarekWo Date: Mon, 16 Mar 2026 21:01:15 +0100 Subject: [PATCH] feat(contacts): show path/route info in UI and split console commands - Console `contacts` now shows device-only contacts with path info (matching meshcore-cli format: name, type, pubkey, path) - New `contacts_all` command shows all contacts (device + cached from DB) - Contact cards in UI now always show routing mode for device contacts (Flood, Direct 0 hop, or hex path with hop count) - Fix path_or_mode computation: prioritize out_path over out_path_len to handle firmware edge case where out_path exists but out_path_len=-1 Co-Authored-By: Claude Opus 4.6 --- app/main.py | 35 +++++++++++++++++++++++++++++++---- app/routes/api.py | 9 ++++++--- app/static/js/contacts.js | 16 +++++++++++++--- 3 files changed, 50 insertions(+), 10 deletions(-) diff --git a/app/main.py b/app/main.py index 4f2bdf5..a2df1a3 100644 --- a/app/main.py +++ b/app/main.py @@ -216,15 +216,41 @@ def _execute_console_command(args: list) -> str: return "No device info available" elif cmd == 'contacts': + # Show device-only contacts with path info (like meshcore-cli) + type_names = {0: 'NONE', 1: 'CLI', 2: 'REP', 3: 'ROOM', 4: 'SENS'} + if not device_manager.mc or not device_manager.mc.contacts: + return "No contacts on device" + try: + device_manager.execute(device_manager.mc.ensure_contacts(follow=True)) + except Exception: + pass # use whatever is in memory + lines = [] + for pk, c in device_manager.mc.contacts.items(): + name = c.get('adv_name', c.get('name', '?')) + typ = type_names.get(c.get('type', 1), '?') + pk_short = pk[:12] + opl = c.get('out_path_len', -1) + if opl == -1: + path_str = 'Flood' + elif opl == 0: + path_str = '0 hop' + else: + path_str = c.get('out_path', f'len:{opl}') + lines.append(f" {name:30} {typ:4} {pk_short} {path_str}") + return f"Contacts ({len(lines)}) on device:\n" + "\n".join(lines) + + elif cmd == 'contacts_all': + # Show all known contacts (device + cached from DB) contacts = device_manager.get_contacts_from_device() if not contacts: return "No contacts" lines = [] for c in contacts: name = c.get('name', '?') - pk = c.get('public_key', '')[:8] - lines.append(f" {name} ({pk}...)") - return f"Contacts ({len(contacts)}):\n" + "\n".join(lines) + pk = c.get('public_key', '')[:12] + source = c.get('source', '') + lines.append(f" {name} ({pk}...) [{source}]") + return f"All contacts ({len(contacts)}):\n" + "\n".join(lines) elif cmd == 'bat': bat = device_manager.get_battery() @@ -356,7 +382,8 @@ def _execute_console_command(args: list) -> str: " status — Connection status, battery, contacts count\n" " stats — Device statistics (uptime, TX/RX, packets)\n" " bat — Battery voltage\n" - " contacts — List all contacts\n" + " contacts — List device contacts with path info\n" + " contacts_all — List all known contacts (device + cached)\n" " channels — List configured channels\n" " chan — Send channel message\n" " msg — Send direct message\n" diff --git a/app/routes/api.py b/app/routes/api.py index 297d1af..bf1a013 100644 --- a/app/routes/api.py +++ b/app/routes/api.py @@ -2378,10 +2378,13 @@ def get_contacts_detailed_api(): # 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: + if out_path: + # out_path present = known route (even if out_path_len says -1) path_or_mode = out_path + elif out_path_len == -1: + path_or_mode = 'Flood' + elif out_path_len == 0: + path_or_mode = '0 hop' else: path_or_mode = f'Path len: {out_path_len}' diff --git a/app/static/js/contacts.js b/app/static/js/contacts.js index a4e5a0a..6ab30e6 100644 --- a/app/static/js/contacts.js +++ b/app/static/js/contacts.js @@ -2142,12 +2142,22 @@ function createExistingContactCard(contact, index) { lastAdvertDiv.appendChild(timeText); } - // Path/mode (optional) + // Path/route info for device contacts let pathDiv = null; - if (contact.path_or_mode && contact.path_or_mode !== 'Flood') { + if (contact.on_device !== false) { pathDiv = document.createElement('div'); pathDiv.className = 'text-muted small'; - pathDiv.textContent = `Path: ${contact.path_or_mode}`; + const mode = contact.path_or_mode || 'Flood'; + const pathLen = contact.out_path_len; + if (mode === 'Flood') { + pathDiv.innerHTML = ' Flood'; + } else if (mode === '0 hop') { + pathDiv.innerHTML = ' Direct (0 hop)'; + } else { + // mode is hex path string, show hops count + path + const hops = pathLen >= 0 ? pathLen : '?'; + pathDiv.innerHTML = ` Path: ${mode} (${hops} hops)`; + } } // Action buttons