mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-06-16 08:05:12 +02:00
feat(channels): merge post-resend echoes into existing repeater badge
PR #4 of 5. After a successful resend, re-arm _pending_echo with the original msg_id and known pkt_payload so echoes from previously-unreached repeaters that pick up the rebroadcast are classified as 'sent' and carry msg_id in the SocketIO emit. The frontend echo handler now collects forced msg_ids and passes them to refreshMessagesMeta(forceIds), which bypasses the "already has route info, skip" guard for those ids. End result: clicking resend extends the repeater list on the existing message's badge in place — no duplicate row, no stale count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+30
-2
@@ -1325,15 +1325,26 @@ class DeviceManager:
|
||||
|
||||
logger.debug(f"Echo ({direction}): path={path} snr={snr} hash_size={hash_size} pkt={pkt_payload[:16]}...")
|
||||
|
||||
# Carry msg_id when the echo was correlated to a sent message —
|
||||
# the UI uses it to force-refresh that specific badge, bypassing
|
||||
# the "already has route info, skip" guard in refreshMessagesMeta.
|
||||
correlated_msg_id = (self._pending_echo.get('msg_id')
|
||||
if self._pending_echo
|
||||
and self._pending_echo.get('pkt_payload') == pkt_payload
|
||||
else None)
|
||||
|
||||
# Emit SocketIO event for real-time UI update
|
||||
if self.socketio:
|
||||
self.socketio.emit('echo', {
|
||||
payload = {
|
||||
'pkt_payload': pkt_payload,
|
||||
'path': path,
|
||||
'snr': snr,
|
||||
'direction': direction,
|
||||
'hash_size': hash_size,
|
||||
}, namespace='/chat')
|
||||
}
|
||||
if correlated_msg_id is not None:
|
||||
payload['msg_id'] = correlated_msg_id
|
||||
self.socketio.emit('echo', payload, namespace='/chat')
|
||||
|
||||
def _is_manual_approval_enabled(self) -> bool:
|
||||
"""Check if manual contact approval is enabled (from database)."""
|
||||
@@ -1800,6 +1811,23 @@ class DeviceManager:
|
||||
logger.warning(f"Resend msg #{msg_id} failed: payload={payload}")
|
||||
return {'success': False, 'error': f'Device rejected resend: {err}'}
|
||||
logger.info(f"Resent channel msg #{msg_id} via CMD_SEND_RAW_PACKET ({len(raw_packet)} bytes)")
|
||||
|
||||
# Re-arm echo correlation so the next 60s of incoming echoes for
|
||||
# this packet hash get classified as 'sent' and carry msg_id in
|
||||
# the SocketIO emit — that's what tells the UI to extend the
|
||||
# repeater list on the existing badge instead of skipping it.
|
||||
stored_pkt_payload = msg.get('pkt_payload')
|
||||
if stored_pkt_payload:
|
||||
with self._echo_lock:
|
||||
self._pending_echo = {
|
||||
'timestamp': time.time(),
|
||||
'channel_idx': msg.get('channel_idx', 0),
|
||||
'msg_id': msg_id,
|
||||
'pkt_payload': stored_pkt_payload,
|
||||
'expected_payloads': {stored_pkt_payload},
|
||||
'guess_pkt_payload': stored_pkt_payload,
|
||||
}
|
||||
|
||||
return {'success': True, 'message': 'Resent', 'id': msg_id, 'bytes': len(raw_packet)}
|
||||
except Exception as e:
|
||||
logger.error(f"resend_channel_message #{msg_id} failed: {e}")
|
||||
|
||||
+24
-9
@@ -455,13 +455,22 @@ function connectChatSocket() {
|
||||
|
||||
// Real-time echo data — update metadata for specific messages (no full reload)
|
||||
let echoRefreshTimer = null;
|
||||
const targetedRefreshIds = new Set(); // msg_ids that must bypass the "already has route" skip
|
||||
chatSocket.on('echo', (data) => {
|
||||
if (currentArchiveDate) return; // Don't refresh archive view
|
||||
// When the backend tags the echo with a specific msg_id (e.g. echoes
|
||||
// arriving after a resend), record it so the debounced refresh
|
||||
// re-fetches that message's meta even if its badge is already drawn.
|
||||
if (data && typeof data.msg_id === 'number') {
|
||||
targetedRefreshIds.add(data.msg_id);
|
||||
}
|
||||
// Debounce: wait for echoes to settle, then update affected messages
|
||||
if (echoRefreshTimer) clearTimeout(echoRefreshTimer);
|
||||
echoRefreshTimer = setTimeout(() => {
|
||||
echoRefreshTimer = null;
|
||||
refreshMessagesMeta();
|
||||
const ids = Array.from(targetedRefreshIds);
|
||||
targetedRefreshIds.clear();
|
||||
refreshMessagesMeta(ids);
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
@@ -1089,23 +1098,29 @@ function appendMessageFromSocket(data) {
|
||||
* Refresh metadata (SNR, hops, route, analyzer) for messages missing it.
|
||||
* Fetches /api/messages/<id>/meta for each incomplete message, updates DOM in-place.
|
||||
*/
|
||||
async function refreshMessagesMeta() {
|
||||
async function refreshMessagesMeta(forceIds = []) {
|
||||
const container = document.getElementById('messagesList');
|
||||
if (!container) return;
|
||||
|
||||
const forced = new Set((forceIds || []).map(String));
|
||||
|
||||
// Find message wrappers that don't have full metadata yet
|
||||
const wrappers = container.querySelectorAll('.message-wrapper[data-msg-id]');
|
||||
for (const wrapper of wrappers) {
|
||||
// Skip messages that already have meta info with route/analyzer data
|
||||
const metaEl = wrapper.querySelector('.message-meta');
|
||||
const actionsEl = wrapper.querySelector('.message-actions');
|
||||
const hasRoute = metaEl && metaEl.querySelector('.path-info');
|
||||
const hasAnalyzer = actionsEl && actionsEl.querySelector('[title="View in Analyzer"]');
|
||||
if (hasRoute && hasAnalyzer) continue;
|
||||
|
||||
const msgId = wrapper.dataset.msgId;
|
||||
if (!msgId || msgId.startsWith('_pending_')) continue;
|
||||
|
||||
// Skip messages that already have meta info with route/analyzer data,
|
||||
// unless this msg_id was explicitly forced (e.g. by post-resend echoes
|
||||
// that need the existing badge re-fetched to extend the repeater list).
|
||||
if (!forced.has(msgId)) {
|
||||
const metaEl = wrapper.querySelector('.message-meta');
|
||||
const actionsEl = wrapper.querySelector('.message-actions');
|
||||
const hasRoute = metaEl && metaEl.querySelector('.path-info');
|
||||
const hasAnalyzer = actionsEl && actionsEl.querySelector('[title="View in Analyzer"]');
|
||||
if (hasRoute && hasAnalyzer) continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch(`/api/messages/${msgId}/meta`);
|
||||
const meta = await resp.json();
|
||||
|
||||
Reference in New Issue
Block a user