From 677036a8316480869cd598757b54230061cbec29 Mon Sep 17 00:00:00 2001 From: MarekWo Date: Sat, 28 Mar 2026 12:52:00 +0100 Subject: [PATCH] fix(dm): move retry counter below message, show delivery info visually MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the attempt counter (e.g. "Attempt 15/24") from next to the status icon to below the message text, left of the Resend button. Add visible delivery meta line for delivered/failed messages showing attempt count and path used. Store attempt info for failed messages too. Replace Polish abbreviations (ŚK, ŚD, ŚG) with English in all log messages. Co-Authored-By: Claude Opus 4.6 --- app/device_manager.py | 31 +++++++++++----------- app/static/css/style.css | 11 ++++++-- app/static/js/dm.js | 56 +++++++++++++++++++++++++--------------- 3 files changed, 60 insertions(+), 38 deletions(-) diff --git a/app/device_manager.py b/app/device_manager.py index b974a25..f9fd677 100644 --- a/app/device_manager.py +++ b/app/device_manager.py @@ -1275,8 +1275,8 @@ class DeviceManager: 4-scenario matrix based on (has_path × has_configured_paths): - Scenario 1: No path, no configured paths → FLOOD only - Scenario 2: Has path, no configured paths → DIRECT + optional FLOOD - - Scenario 3: No path, has configured paths → FLOOD first, then ŚD rotation - - Scenario 4: Has path, has configured paths → DIRECT on ŚK, ŚD rotation, optional FLOOD + - Scenario 3: No path, has configured paths → FLOOD first, then configured path rotation + - Scenario 4: Has path, has configured paths → DIRECT on current path, configured path rotation, optional FLOOD The no_auto_flood per-contact flag prevents automatic DIRECT→FLOOD reset in Scenarios 2 and 4. Ignored in Scenarios 1 and 3. @@ -1405,7 +1405,7 @@ class DeviceManager: # Scenario 2: Has path, no configured paths → DIRECT + optional FLOOD # ════════════════════════════════════════════════════════════ elif has_path and not has_configured_paths: - # Phase 1: Direct retries on current ŚK + # Phase 1: Direct retries on current path for _ in range(cfg['direct_max_retries']): attempt += 1 if await _retry(attempt, float(cfg['direct_interval'])): @@ -1425,7 +1425,7 @@ class DeviceManager: return # ════════════════════════════════════════════════════════════ - # Scenario 3: No path, has configured paths → FLOOD first, then ŚD rotation + # Scenario 3: No path, has configured paths → FLOOD first, then configured path rotation # ════════════════════════════════════════════════════════════ elif not has_path and has_configured_paths: # Phase 1: FLOOD retries per NoPath settings (discover new path) @@ -1433,9 +1433,9 @@ class DeviceManager: for _ in range(cfg['flood_max_retries']): attempt += 1 if await _retry(attempt, float(cfg['flood_interval'])): - return # Firmware sets discovered path as ŚK + return # Firmware sets discovered path automatically - # Phase 2: ŚD rotation (primary first, then others by sort_order) + # Phase 2: Configured path rotation (primary first, then others by sort_order) logger.info("DM retry: FLOOD exhausted, rotating through configured paths") direct_interval = float(cfg['direct_interval']) @@ -1455,28 +1455,28 @@ class DeviceManager: await self._restore_primary_path(contact, contact_pubkey) return - # Restore ŚG regardless of outcome + # Restore primary path regardless of outcome await self._restore_primary_path(contact, contact_pubkey) # ════════════════════════════════════════════════════════════ - # Scenario 4: Has path + has configured paths → DIRECT on ŚK, ŚD rotation, optional FLOOD + # Scenario 4: Has path + has configured paths → DIRECT on current path, configured path rotation, optional FLOOD # ════════════════════════════════════════════════════════════ else: # has_path and has_configured_paths - # Phase 1: Direct retries on current ŚK + # Phase 1: Direct retries on current path for _ in range(cfg['direct_max_retries']): attempt += 1 if await _retry(attempt, float(cfg['direct_interval'])): - return # Delivered on ŚK, no path change needed + return # Delivered on current path, no change needed - # Phase 2: ŚD rotation with dedup - logger.info("DM retry: direct on ŚK exhausted, rotating through configured paths") + # Phase 2: Configured path rotation with dedup + logger.info("DM retry: direct retries exhausted, rotating through configured paths") direct_interval = float(cfg['direct_interval']) for path_info in rotation_order: - # Dedup: skip if this configured path matches original ŚK + # Dedup: skip if this configured path matches original device path if self._paths_match(original_out_path, original_out_path_len, path_info): logger.debug(f"DM retry: skipping path '{path_info.get('label', '')}' " - f"({path_info['path_hex']}) — matches current ŚK") + f"({path_info['path_hex']}) — matches current device path") continue try: @@ -1508,10 +1508,11 @@ class DeviceManager: await self._restore_primary_path(contact, contact_pubkey) return - # Restore ŚG regardless of outcome + # Restore primary path regardless of outcome 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_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/css/style.css b/app/static/css/style.css index 0d9e8a5..dc9dc39 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -739,9 +739,15 @@ main { } .dm-retry-info { - font-size: 0.6rem; + font-size: 0.65rem; color: var(--text-meta); - margin-left: 0.15rem; + margin-right: auto; +} + +.dm-delivery-meta { + font-size: 0.65rem; + color: var(--text-meta); + margin-top: 0.1rem; } .dm-delivery-popup { @@ -763,6 +769,7 @@ main { .dm-actions { display: flex; justify-content: flex-end; + align-items: center; margin-top: 0.25rem; } diff --git a/app/static/js/dm.js b/app/static/js/dm.js index 4451bc7..b3497a1 100644 --- a/app/static/js/dm.js +++ b/app/static/js/dm.js @@ -112,14 +112,14 @@ function connectChatSocket() { 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'; - // Remove retry counter if present + // Unwrap status icon from wrapper span const wrapper = statusEl.closest('[data-dm-id]'); if (wrapper) { - const info = wrapper.querySelector('.dm-retry-info'); - if (info) info.remove(); - // Unwrap: replace wrapper span with just the icon wrapper.replaceWith(statusEl); } + // Clear retry counter in actions area + const retryInfo = el.querySelector('.dm-retry-info'); + if (retryInfo) retryInfo.textContent = ''; } }); }); @@ -127,27 +127,27 @@ function connectChatSocket() { // Real-time DM retry progress chatSocket.on('dm_retry_status', (data) => { if (!data.dm_id) return; - const wrapper = document.querySelector(`[data-dm-id="${data.dm_id}"]`); - if (!wrapper) return; - const info = wrapper.querySelector('.dm-retry-info'); - if (info) info.textContent = `${data.attempt}/${data.max_attempts}`; + const info = document.querySelector(`.dm-retry-info[data-dm-id="${data.dm_id}"]`); + if (info) info.textContent = `Attempt ${data.attempt}/${data.max_attempts}`; }); // DM retry exhausted — mark as failed chatSocket.on('dm_retry_failed', (data) => { if (!data.dm_id) return; - const wrapper = document.querySelector(`[data-dm-id="${data.dm_id}"]`); - if (!wrapper) return; - const icon = wrapper.querySelector('.dm-status'); - if (icon) { - icon.className = 'bi bi-x-circle dm-status timeout'; - icon.title = 'Delivery failed — all retries exhausted'; + // Update status icon + const wrapper = document.querySelector(`.dm-status-unknown[data-dm-id="${data.dm_id}"]`); + if (wrapper) { + const icon = wrapper.querySelector('.dm-status'); + if (icon) { + icon.className = 'bi bi-x-circle dm-status timeout'; + icon.title = 'Delivery failed — all retries exhausted'; + } + wrapper.removeAttribute('onclick'); + wrapper.classList.remove('dm-status-unknown'); } - const info = wrapper.querySelector('.dm-retry-info'); - if (info) info.remove(); - // Remove onclick - wrapper.removeAttribute('onclick'); - wrapper.classList.remove('dm-status-unknown'); + // Clear retry counter + const info = document.querySelector(`.dm-retry-info[data-dm-id="${data.dm_id}"]`); + if (info) info.textContent = ''; }); // Real-time device status @@ -1141,7 +1141,7 @@ function displayMessages(messages) { statusIcon = ``; } else { // No ACK received — show clickable "?" with retry counter - statusIcon = ``; + statusIcon = ``; } } @@ -1157,9 +1157,22 @@ function displayMessages(messages) { } } - // Resend button for own messages + // Delivery info for delivered/failed messages (attempt count + path) + let deliveryMeta = ''; + if (msg.is_own && (msg.status === 'delivered' || msg.status === 'failed') + && (msg.delivery_attempt || msg.delivery_path)) { + 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}`); + deliveryMeta = `
${parts.join(', ')}
`; + } + + // Resend button for own messages (with retry counter placeholder) const resendBtn = msg.is_own ? `
+ @@ -1172,6 +1185,7 @@ function displayMessages(messages) { ${statusIcon}
${processMessageContent(msg.content)}
+ ${deliveryMeta} ${meta} ${resendBtn} `;