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 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2026-03-16 21:01:15 +01:00
parent fa8190923f
commit 5f72f40742
3 changed files with 50 additions and 10 deletions

View File

@@ -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 <idx> <msg> — Send channel message\n"
" msg <name> <msg> — Send direct message\n"

View File

@@ -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}'

View File

@@ -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 = '<i class="bi bi-broadcast"></i> Flood';
} else if (mode === '0 hop') {
pathDiv.innerHTML = '<i class="bi bi-arrow-right-short"></i> Direct (0 hop)';
} else {
// mode is hex path string, show hops count + path
const hops = pathLen >= 0 ? pathLen : '?';
pathDiv.innerHTML = `<i class="bi bi-signpost-split"></i> Path: ${mode} (${hops} hops)`;
}
}
// Action buttons