From ebfe383190b9e172617a043822ea605249cea761 Mon Sep 17 00:00:00 2001 From: MarekWo Date: Sun, 1 Mar 2026 17:19:54 +0100 Subject: [PATCH] feat(v2): Add SocketIO real-time push for DM page - Add SocketIO /chat client to dm.js for real-time DM and ACK updates - Listen for new_message (dm), ack, device_status events - Remove 5x cascading refresh after send (replaced by SocketIO ACK) - Reduce polling interval from 10s to 60s (fallback only) - Add data-ack attribute to status icons for real-time ACK updates - Enrich ACK emission with snr/rssi/route_type (device_manager.py) - Include socket.io.min.js in dm.html template Co-Authored-By: Claude Opus 4.6 --- app/device_manager.py | 3 ++ app/static/js/dm.js | 100 +++++++++++++++++++++++++++++++++--------- app/templates/dm.html | 3 ++ 3 files changed, 85 insertions(+), 21 deletions(-) diff --git a/app/device_manager.py b/app/device_manager.py index d2f9649..fdea069 100644 --- a/app/device_manager.py +++ b/app/device_manager.py @@ -395,6 +395,9 @@ class DeviceManager: if self.socketio: self.socketio.emit('ack', { 'expected_ack': expected_ack, + 'snr': data.get('snr'), + 'rssi': data.get('rssi'), + 'route_type': data.get('route_type', ''), }, namespace='/chat') except Exception as e: diff --git a/app/static/js/dm.js b/app/static/js/dm.js index 51ad209..e926262 100644 --- a/app/static/js/dm.js +++ b/app/static/js/dm.js @@ -11,6 +11,7 @@ let contactsList = []; // List of all contacts from device let dmLastSeenTimestamps = {}; let autoRefreshInterval = null; let lastMessageTimestamp = 0; // Track latest message timestamp for smart refresh +let chatSocket = null; // SocketIO connection to /chat namespace /** * Get display-friendly name (truncate full pubkeys to short prefix) @@ -21,6 +22,72 @@ function displayName(name) { return name; } +/** + * Connect to SocketIO /chat namespace for real-time DM and ACK updates + */ +function connectChatSocket() { + if (typeof io === 'undefined') { + console.warn('SocketIO not available, falling back to polling only'); + return; + } + + const wsUrl = window.location.origin; + chatSocket = io(wsUrl + '/chat', { + transports: ['websocket', 'polling'], + reconnection: true, + reconnectionDelay: 2000, + reconnectionDelayMax: 10000, + }); + + chatSocket.on('connect', () => { + console.log('DM: SocketIO connected to /chat'); + updateStatus('connected'); + }); + + chatSocket.on('disconnect', () => { + console.log('DM: SocketIO disconnected'); + }); + + // Real-time new DM message + chatSocket.on('new_message', (data) => { + if (data.type !== 'dm') return; + + const msgPubkey = data.contact_pubkey || ''; + const currentPubkey = currentConversationId ? currentConversationId.replace('pk_', '') : ''; + + if (currentPubkey && msgPubkey.startsWith(currentPubkey)) { + // Message is for current conversation — reload messages + loadMessages(); + } else { + // Message is for a different conversation — refresh conversation list + loadConversations(); + } + }); + + // Real-time ACK delivery confirmation + chatSocket.on('ack', (data) => { + if (!data.expected_ack) return; + + // Find message with matching expected_ack in DOM and update status + const msgElements = document.querySelectorAll('#dmMessagesList .dm-message.own'); + msgElements.forEach(el => { + const statusEl = el.querySelector(`.dm-status[data-ack="${data.expected_ack}"]`); + if (statusEl) { + statusEl.className = 'bi bi-check2 dm-status delivered'; + const tooltip = []; + if (data.snr != null) tooltip.push(`SNR: ${data.snr}`); + if (data.route_type) tooltip.push(`Route: ${data.route_type}`); + statusEl.title = tooltip.length > 0 ? tooltip.join(', ') : 'Delivered'; + } + }); + }); + + // Real-time device status + chatSocket.on('device_status', (data) => { + updateStatus(data.connected ? 'connected' : 'disconnected'); + }); +} + // Initialize on page load document.addEventListener('DOMContentLoaded', async function() { console.log('DM page initialized'); @@ -70,7 +137,10 @@ document.addEventListener('DOMContentLoaded', async function() { // Load auto-retry config loadAutoRetryConfig(); - // Setup auto-refresh + // Connect SocketIO for real-time updates + connectChatSocket(); + + // Setup auto-refresh (fallback, 60s interval since SocketIO handles primary updates) setupAutoRefresh(); }); @@ -440,18 +510,19 @@ function displayMessages(messages) { // Status icon for own messages let statusIcon = ''; if (msg.is_own) { + const ackAttr = msg.expected_ack ? ` data-ack="${msg.expected_ack}"` : ''; if (msg.status === 'delivered') { let title = 'Delivered'; if (msg.delivery_snr !== null && msg.delivery_snr !== undefined) { title += `, SNR: ${msg.delivery_snr.toFixed(1)} dB`; } if (msg.delivery_route) title += ` (${msg.delivery_route})`; - statusIcon = ``; + statusIcon = ``; } else if (msg.status === 'pending') { - statusIcon = ''; + statusIcon = ``; } else { // No ACK received — show clickable "?" with explanation - statusIcon = ``; + statusIcon = ``; } } @@ -529,22 +600,9 @@ async function sendMessage() { updateCharCounter(); showNotification('Message sent', 'success'); - // Reload messages to show sent message + ACK delivery status - // Extended delays to cover auto-retry window (retries happen in background) - const ackRefreshDelays = [2000, 8000, 20000, 40000, 60000]; - let ackRefreshIdx = 0; - const scheduleAckRefresh = () => { - if (ackRefreshIdx >= ackRefreshDelays.length) return; - const delay = ackRefreshDelays[ackRefreshIdx++]; - setTimeout(async () => { - await loadMessages(); - const ownMsgs = document.querySelectorAll('#dmMessagesList .dm-message.own'); - const lastOwn = ownMsgs.length > 0 ? ownMsgs[ownMsgs.length - 1] : null; - const delivered = lastOwn && lastOwn.querySelector('.dm-status.delivered'); - if (!delivered) scheduleAckRefresh(); - }, delay); - }; - scheduleAckRefresh(); + // Reload messages once to show sent message + // ACK delivery updates arrive via SocketIO in real-time + await loadMessages(); } else { showNotification('Failed to send: ' + data.error, 'danger'); } @@ -562,7 +620,7 @@ async function sendMessage() { * Only refreshes UI when new messages arrive */ function setupAutoRefresh() { - const checkInterval = 10000; // 10 seconds + const checkInterval = 60000; // 60 seconds (SocketIO handles real-time updates) autoRefreshInterval = setInterval(async () => { // Reload conversations to update unread indicators diff --git a/app/templates/dm.html b/app/templates/dm.html index 16c3811..970f1f1 100644 --- a/app/templates/dm.html +++ b/app/templates/dm.html @@ -186,6 +186,9 @@ + + +