diff --git a/app/device_manager.py b/app/device_manager.py index f9fd677..e3065d5 100644 --- a/app/device_manager.py +++ b/app/device_manager.py @@ -1355,8 +1355,17 @@ class DeviceManager: if not no_auto_flood: max_attempts += cfg['flood_max_retries'] - # Track current path description for delivery info - path_desc = "FLOOD" if not has_path else "DIRECT" + # Track current path hex for delivery info (actual route, not label) + def _extract_path_hex(out_path, out_path_len): + """Extract meaningful hex portion from device path.""" + if out_path_len <= 0 or not out_path: + return '' + hop_count = out_path_len & 0x3F + hash_size = (out_path_len >> 6) + 1 + meaningful_len = hop_count * hash_size * 2 + return out_path[:meaningful_len].lower() if meaningful_len > 0 else '' + + path_desc = _extract_path_hex(original_out_path, original_out_path_len) if has_path else '' logger.info(f"DM retry task started: dm_id={dm_id}, scenario={scenario}, " f"configured_paths={len(configured_paths)}, no_auto_flood={no_auto_flood}, " @@ -1373,6 +1382,13 @@ class DeviceManager: if delivered: self.db.update_dm_delivery_info( dm_id, display, max_attempts, path_desc) + if self.socketio: + self.socketio.emit('dm_delivered_info', { + 'dm_id': dm_id, + 'attempt': display, + 'max_attempts': max_attempts, + 'path': path_desc, + }, namespace='/chat') return delivered # ── Emit status for initial send (attempt 1) and wait for ACK ── @@ -1387,6 +1403,11 @@ class DeviceManager: if ack_event: self._confirm_delivery(dm_id, initial_ack, ack_event) self.db.update_dm_delivery_info(dm_id, 1, max_attempts, path_desc) + if self.socketio: + self.socketio.emit('dm_delivered_info', { + 'dm_id': dm_id, 'attempt': 1, + 'max_attempts': max_attempts, 'path': path_desc, + }, namespace='/chat') return logger.debug(f"DM retry: initial ACK not received (timeout)") @@ -1418,7 +1439,7 @@ class DeviceManager: logger.info("DM retry: direct exhausted, resetting to FLOOD") except Exception: pass - path_desc = "FLOOD" + path_desc = '' for _ in range(cfg['direct_flood_retries']): attempt += 1 if await _retry(attempt, float(cfg['flood_interval'])): @@ -1443,7 +1464,7 @@ class DeviceManager: try: await self._change_path_async(contact, path_info['path_hex'], path_info['hash_size']) label = path_info.get('label', '') - path_desc = f"{label} ({path_info['path_hex']})" if label else path_info['path_hex'] + path_desc = path_info['path_hex'] logger.info(f"DM retry: switched to path '{label}' ({path_info['path_hex']})") except Exception as e: logger.warning(f"DM retry: failed to switch path: {e}") @@ -1482,7 +1503,7 @@ class DeviceManager: try: await self._change_path_async(contact, path_info['path_hex'], path_info['hash_size']) label = path_info.get('label', '') - path_desc = f"{label} ({path_info['path_hex']})" if label else path_info['path_hex'] + path_desc = path_info['path_hex'] logger.info(f"DM retry: switched to path '{label}' ({path_info['path_hex']})") except Exception as e: logger.warning(f"DM retry: failed to switch path: {e}") @@ -1501,7 +1522,7 @@ class DeviceManager: logger.info("DM retry: all paths exhausted, falling back to FLOOD") except Exception: pass - path_desc = "FLOOD" + path_desc = '' for _ in range(cfg['flood_max_retries']): attempt += 1 if await _retry(attempt, float(cfg['flood_interval'])): @@ -1512,7 +1533,7 @@ class DeviceManager: await self._restore_primary_path(contact, contact_pubkey) # ── Common epilogue: mark failed, grace period for late ACKs ── - self.db.update_dm_delivery_info(dm_id, attempt + 1, max_attempts, path_desc) + self.db.update_dm_delivery_info(dm_id, attempt + 1, max_attempts, '') self.db.update_dm_delivery_status(dm_id, 'failed') self._emit_retry_failed(dm_id, initial_ack) logger.warning(f"DM retry exhausted ({attempt + 1} total attempts, scenario={scenario}) " diff --git a/app/static/js/dm.js b/app/static/js/dm.js index b3497a1..448b959 100644 --- a/app/static/js/dm.js +++ b/app/static/js/dm.js @@ -150,6 +150,31 @@ function connectChatSocket() { if (info) info.textContent = ''; }); + // Real-time delivery info — show attempt count + route after successful delivery + chatSocket.on('dm_delivered_info', (data) => { + if (!data.dm_id) return; + // Find the message element containing this dm_id + const retryEl = document.querySelector(`.dm-retry-info[data-dm-id="${data.dm_id}"]`); + if (!retryEl) return; + retryEl.textContent = ''; + const msgDiv = retryEl.closest('.dm-message'); + if (!msgDiv) return; + // Build delivery meta text + const parts = []; + if (data.attempt && data.max_attempts) parts.push(`Attempt ${data.attempt}/${data.max_attempts}`); + if (data.path) parts.push(`Route: ${formatDmRoute(data.path)}`); + if (parts.length > 0) { + let metaEl = msgDiv.querySelector('.dm-delivery-meta'); + if (!metaEl) { + metaEl = document.createElement('div'); + metaEl.className = 'dm-delivery-meta'; + const contentDiv = msgDiv.querySelector('div:nth-child(2)'); + if (contentDiv) contentDiv.after(metaEl); + } + metaEl.textContent = parts.join(', '); + } + }); + // Real-time device status chatSocket.on('device_status', (data) => { updateStatus(data.connected ? 'connected' : 'disconnected'); @@ -1129,7 +1154,7 @@ function displayMessages(messages) { if (msg.delivery_attempt && msg.delivery_max_attempts) { title += ` (${msg.delivery_attempt}/${msg.delivery_max_attempts})`; } - if (msg.delivery_path) title += `, Path: ${msg.delivery_path}`; + if (msg.delivery_path) title += `, Route: ${formatDmRoute(msg.delivery_path)}`; if (msg.delivery_snr !== null && msg.delivery_snr !== undefined) { title += `, SNR: ${msg.delivery_snr.toFixed(1)} dB`; } @@ -1157,15 +1182,18 @@ function displayMessages(messages) { } } - // Delivery info for delivered/failed messages (attempt count + path) + // Delivery info for delivered/failed messages (attempt count + route) let deliveryMeta = ''; if (msg.is_own && (msg.status === 'delivered' || msg.status === 'failed') - && (msg.delivery_attempt || msg.delivery_path)) { + && msg.delivery_attempt) { const parts = []; if (msg.delivery_attempt && msg.delivery_max_attempts) { parts.push(`Attempt ${msg.delivery_attempt}/${msg.delivery_max_attempts}`); } - if (msg.delivery_path) parts.push(`Path: ${msg.delivery_path}`); + // Show route only for delivered messages (not failed) + if (msg.status === 'delivered' && msg.delivery_path) { + parts.push(`Route: ${formatDmRoute(msg.delivery_path)}`); + } deliveryMeta = `
`; } @@ -1335,6 +1363,20 @@ function resendMessage(content) { input.focus(); } +/** + * Format a hex path as route string (e.g. "5e34e761" → "5e→34→e7→61") + * Truncates if more than 4 segments. + */ +function formatDmRoute(hexPath) { + if (!hexPath) return ''; + const segments = hexPath.match(/.{1,2}/g) || []; + if (segments.length === 0) return ''; + if (segments.length > 4) { + return `${segments[0]}\u2192...\u2192${segments[segments.length - 1]}`; + } + return segments.join('\u2192'); +} + /** * Show delivery info popup (mobile-friendly, same pattern as showPathPopup) */