diff --git a/app/services/message_send.py b/app/services/message_send.py index 8b41675f..82683289 100644 --- a/app/services/message_send.py +++ b/app/services/message_send.py @@ -20,6 +20,11 @@ from app.services.messages import ( logger = logging.getLogger(__name__) +NO_RADIO_RESPONSE_AFTER_SEND_DETAIL = ( + "Send command was issued to the radio, but no response was heard back. " + "The message may or may not have sent successfully." +) + BroadcastFn = Callable[..., Any] TrackAckFn = Callable[[str, int, int], bool] NowFn = Callable[[], float] @@ -279,7 +284,14 @@ async def send_direct_message_to_contact( timestamp=sender_timestamp, ) - if result is None or result.type == EventType.ERROR: + if result is None: + logger.warning( + "No response from radio after direct send to %s; send outcome is unknown", + contact.public_key[:12], + ) + raise HTTPException(status_code=504, detail=NO_RADIO_RESPONSE_AFTER_SEND_DETAIL) + + if result.type == EventType.ERROR: raise HTTPException(status_code=500, detail=f"Failed to send message: {result.payload}") message = await create_outgoing_direct_message( diff --git a/frontend/prebuilt/assets/BotCodeEditor-ycUmdsPQ.js b/frontend/prebuilt/assets/BotCodeEditor-C20Gx7-Q.js similarity index 99% rename from frontend/prebuilt/assets/BotCodeEditor-ycUmdsPQ.js rename to frontend/prebuilt/assets/BotCodeEditor-C20Gx7-Q.js index 6b9d79a5..ccf03948 100644 Binary files a/frontend/prebuilt/assets/BotCodeEditor-ycUmdsPQ.js and b/frontend/prebuilt/assets/BotCodeEditor-C20Gx7-Q.js differ diff --git a/frontend/prebuilt/assets/BotCodeEditor-ycUmdsPQ.js.map b/frontend/prebuilt/assets/BotCodeEditor-C20Gx7-Q.js.map similarity index 99% rename from frontend/prebuilt/assets/BotCodeEditor-ycUmdsPQ.js.map rename to frontend/prebuilt/assets/BotCodeEditor-C20Gx7-Q.js.map index b9331451..ec556d0f 100644 Binary files a/frontend/prebuilt/assets/BotCodeEditor-ycUmdsPQ.js.map and b/frontend/prebuilt/assets/BotCodeEditor-C20Gx7-Q.js.map differ diff --git a/frontend/prebuilt/assets/CrackerPanel-_94u2vQ6.js b/frontend/prebuilt/assets/CrackerPanel-D45MeFE8.js similarity index 99% rename from frontend/prebuilt/assets/CrackerPanel-_94u2vQ6.js rename to frontend/prebuilt/assets/CrackerPanel-D45MeFE8.js index 4ecec405..7666fea3 100644 Binary files a/frontend/prebuilt/assets/CrackerPanel-_94u2vQ6.js and b/frontend/prebuilt/assets/CrackerPanel-D45MeFE8.js differ diff --git a/frontend/prebuilt/assets/CrackerPanel-_94u2vQ6.js.map b/frontend/prebuilt/assets/CrackerPanel-D45MeFE8.js.map similarity index 99% rename from frontend/prebuilt/assets/CrackerPanel-_94u2vQ6.js.map rename to frontend/prebuilt/assets/CrackerPanel-D45MeFE8.js.map index b84fd1a2..6dcc4fc3 100644 Binary files a/frontend/prebuilt/assets/CrackerPanel-_94u2vQ6.js.map and b/frontend/prebuilt/assets/CrackerPanel-D45MeFE8.js.map differ diff --git a/frontend/prebuilt/assets/MapView-Bv8f-A5r.js b/frontend/prebuilt/assets/MapView-D0gphXtL.js similarity index 92% rename from frontend/prebuilt/assets/MapView-Bv8f-A5r.js rename to frontend/prebuilt/assets/MapView-D0gphXtL.js index 84edba74..f8256f16 100644 Binary files a/frontend/prebuilt/assets/MapView-Bv8f-A5r.js and b/frontend/prebuilt/assets/MapView-D0gphXtL.js differ diff --git a/frontend/prebuilt/assets/MapView-Bv8f-A5r.js.map b/frontend/prebuilt/assets/MapView-D0gphXtL.js.map similarity index 99% rename from frontend/prebuilt/assets/MapView-Bv8f-A5r.js.map rename to frontend/prebuilt/assets/MapView-D0gphXtL.js.map index 5b9c1dc1..b51ff89c 100644 Binary files a/frontend/prebuilt/assets/MapView-Bv8f-A5r.js.map and b/frontend/prebuilt/assets/MapView-D0gphXtL.js.map differ diff --git a/frontend/prebuilt/assets/NeighborsMiniMap-DOCpkCDv.js b/frontend/prebuilt/assets/NeighborsMiniMap-zqanKONo.js similarity index 85% rename from frontend/prebuilt/assets/NeighborsMiniMap-DOCpkCDv.js rename to frontend/prebuilt/assets/NeighborsMiniMap-zqanKONo.js index 99a54702..be611bc7 100644 Binary files a/frontend/prebuilt/assets/NeighborsMiniMap-DOCpkCDv.js and b/frontend/prebuilt/assets/NeighborsMiniMap-zqanKONo.js differ diff --git a/frontend/prebuilt/assets/NeighborsMiniMap-DOCpkCDv.js.map b/frontend/prebuilt/assets/NeighborsMiniMap-zqanKONo.js.map similarity index 97% rename from frontend/prebuilt/assets/NeighborsMiniMap-DOCpkCDv.js.map rename to frontend/prebuilt/assets/NeighborsMiniMap-zqanKONo.js.map index f25d9a25..c98d37b8 100644 Binary files a/frontend/prebuilt/assets/NeighborsMiniMap-DOCpkCDv.js.map and b/frontend/prebuilt/assets/NeighborsMiniMap-zqanKONo.js.map differ diff --git a/frontend/prebuilt/assets/PathRouteMap-COU38t0B.js b/frontend/prebuilt/assets/PathRouteMap-kT7xfoMb.js similarity index 95% rename from frontend/prebuilt/assets/PathRouteMap-COU38t0B.js rename to frontend/prebuilt/assets/PathRouteMap-kT7xfoMb.js index be963ee8..d4189b48 100644 Binary files a/frontend/prebuilt/assets/PathRouteMap-COU38t0B.js and b/frontend/prebuilt/assets/PathRouteMap-kT7xfoMb.js differ diff --git a/frontend/prebuilt/assets/PathRouteMap-COU38t0B.js.map b/frontend/prebuilt/assets/PathRouteMap-kT7xfoMb.js.map similarity index 99% rename from frontend/prebuilt/assets/PathRouteMap-COU38t0B.js.map rename to frontend/prebuilt/assets/PathRouteMap-kT7xfoMb.js.map index 48416a70..c2c52770 100644 Binary files a/frontend/prebuilt/assets/PathRouteMap-COU38t0B.js.map and b/frontend/prebuilt/assets/PathRouteMap-kT7xfoMb.js.map differ diff --git a/frontend/prebuilt/assets/Popup-CZeo1EzJ.js b/frontend/prebuilt/assets/Popup-C-Q9ygBf.js similarity index 81% rename from frontend/prebuilt/assets/Popup-CZeo1EzJ.js rename to frontend/prebuilt/assets/Popup-C-Q9ygBf.js index 08e839ed..a6b954ed 100644 Binary files a/frontend/prebuilt/assets/Popup-CZeo1EzJ.js and b/frontend/prebuilt/assets/Popup-C-Q9ygBf.js differ diff --git a/frontend/prebuilt/assets/Popup-CZeo1EzJ.js.map b/frontend/prebuilt/assets/Popup-C-Q9ygBf.js.map similarity index 97% rename from frontend/prebuilt/assets/Popup-CZeo1EzJ.js.map rename to frontend/prebuilt/assets/Popup-C-Q9ygBf.js.map index 852cbabd..5f4d19a0 100644 Binary files a/frontend/prebuilt/assets/Popup-CZeo1EzJ.js.map and b/frontend/prebuilt/assets/Popup-C-Q9ygBf.js.map differ diff --git a/frontend/prebuilt/assets/RepeaterDashboard-CCs38SPq.js b/frontend/prebuilt/assets/RepeaterDashboard-DtQKwbsT.js similarity index 98% rename from frontend/prebuilt/assets/RepeaterDashboard-CCs38SPq.js rename to frontend/prebuilt/assets/RepeaterDashboard-DtQKwbsT.js index 32e68039..0bed4bde 100644 Binary files a/frontend/prebuilt/assets/RepeaterDashboard-CCs38SPq.js and b/frontend/prebuilt/assets/RepeaterDashboard-DtQKwbsT.js differ diff --git a/frontend/prebuilt/assets/RepeaterDashboard-CCs38SPq.js.map b/frontend/prebuilt/assets/RepeaterDashboard-DtQKwbsT.js.map similarity index 99% rename from frontend/prebuilt/assets/RepeaterDashboard-CCs38SPq.js.map rename to frontend/prebuilt/assets/RepeaterDashboard-DtQKwbsT.js.map index 357ea400..3208dbf4 100644 Binary files a/frontend/prebuilt/assets/RepeaterDashboard-CCs38SPq.js.map and b/frontend/prebuilt/assets/RepeaterDashboard-DtQKwbsT.js.map differ diff --git a/frontend/prebuilt/assets/SearchView-8mOuDvjT.js b/frontend/prebuilt/assets/SearchView-DBqP6hD6.js similarity index 97% rename from frontend/prebuilt/assets/SearchView-8mOuDvjT.js rename to frontend/prebuilt/assets/SearchView-DBqP6hD6.js index 0b174763..f28b3310 100644 Binary files a/frontend/prebuilt/assets/SearchView-8mOuDvjT.js and b/frontend/prebuilt/assets/SearchView-DBqP6hD6.js differ diff --git a/frontend/prebuilt/assets/SearchView-8mOuDvjT.js.map b/frontend/prebuilt/assets/SearchView-DBqP6hD6.js.map similarity index 99% rename from frontend/prebuilt/assets/SearchView-8mOuDvjT.js.map rename to frontend/prebuilt/assets/SearchView-DBqP6hD6.js.map index 5bcf0716..126eb768 100644 Binary files a/frontend/prebuilt/assets/SearchView-8mOuDvjT.js.map and b/frontend/prebuilt/assets/SearchView-DBqP6hD6.js.map differ diff --git a/frontend/prebuilt/assets/SettingsModal-D8B2Xdsr.js b/frontend/prebuilt/assets/SettingsModal-BPGiDC12.js similarity index 99% rename from frontend/prebuilt/assets/SettingsModal-D8B2Xdsr.js rename to frontend/prebuilt/assets/SettingsModal-BPGiDC12.js index a62dfef9..6b64f277 100644 Binary files a/frontend/prebuilt/assets/SettingsModal-D8B2Xdsr.js and b/frontend/prebuilt/assets/SettingsModal-BPGiDC12.js differ diff --git a/frontend/prebuilt/assets/SettingsModal-D8B2Xdsr.js.map b/frontend/prebuilt/assets/SettingsModal-BPGiDC12.js.map similarity index 99% rename from frontend/prebuilt/assets/SettingsModal-D8B2Xdsr.js.map rename to frontend/prebuilt/assets/SettingsModal-BPGiDC12.js.map index 537e54a0..9ae08a54 100644 Binary files a/frontend/prebuilt/assets/SettingsModal-D8B2Xdsr.js.map and b/frontend/prebuilt/assets/SettingsModal-BPGiDC12.js.map differ diff --git a/frontend/prebuilt/assets/VisualizerView-DHGA0_mI.js b/frontend/prebuilt/assets/VisualizerView-Cv9KCsSr.js similarity index 99% rename from frontend/prebuilt/assets/VisualizerView-DHGA0_mI.js rename to frontend/prebuilt/assets/VisualizerView-Cv9KCsSr.js index ba6ec87c..92a61cf1 100644 Binary files a/frontend/prebuilt/assets/VisualizerView-DHGA0_mI.js and b/frontend/prebuilt/assets/VisualizerView-Cv9KCsSr.js differ diff --git a/frontend/prebuilt/assets/VisualizerView-DHGA0_mI.js.map b/frontend/prebuilt/assets/VisualizerView-Cv9KCsSr.js.map similarity index 99% rename from frontend/prebuilt/assets/VisualizerView-DHGA0_mI.js.map rename to frontend/prebuilt/assets/VisualizerView-Cv9KCsSr.js.map index 568f5bea..1fa57bf5 100644 Binary files a/frontend/prebuilt/assets/VisualizerView-DHGA0_mI.js.map and b/frontend/prebuilt/assets/VisualizerView-Cv9KCsSr.js.map differ diff --git a/frontend/prebuilt/assets/hooks-BCaVWsWl.js b/frontend/prebuilt/assets/hooks-BCaVWsWl.js deleted file mode 100644 index 43d01373..00000000 Binary files a/frontend/prebuilt/assets/hooks-BCaVWsWl.js and /dev/null differ diff --git a/frontend/prebuilt/assets/hooks-LL30L9PN.js b/frontend/prebuilt/assets/hooks-LL30L9PN.js new file mode 100644 index 00000000..7860626a Binary files /dev/null and b/frontend/prebuilt/assets/hooks-LL30L9PN.js differ diff --git a/frontend/prebuilt/assets/hooks-BCaVWsWl.js.map b/frontend/prebuilt/assets/hooks-LL30L9PN.js.map similarity index 90% rename from frontend/prebuilt/assets/hooks-BCaVWsWl.js.map rename to frontend/prebuilt/assets/hooks-LL30L9PN.js.map index ee830e5b..91ed0a43 100644 Binary files a/frontend/prebuilt/assets/hooks-BCaVWsWl.js.map and b/frontend/prebuilt/assets/hooks-LL30L9PN.js.map differ diff --git a/frontend/prebuilt/assets/index-pnVde5Cl.js b/frontend/prebuilt/assets/index-Fnq3QmNw.js similarity index 88% rename from frontend/prebuilt/assets/index-pnVde5Cl.js rename to frontend/prebuilt/assets/index-Fnq3QmNw.js index 269c07c5..83ebf8ea 100644 Binary files a/frontend/prebuilt/assets/index-pnVde5Cl.js and b/frontend/prebuilt/assets/index-Fnq3QmNw.js differ diff --git a/frontend/prebuilt/assets/index-Fnq3QmNw.js.map b/frontend/prebuilt/assets/index-Fnq3QmNw.js.map new file mode 100644 index 00000000..8b8f914c Binary files /dev/null and b/frontend/prebuilt/assets/index-Fnq3QmNw.js.map differ diff --git a/frontend/prebuilt/assets/index-pnVde5Cl.js.map b/frontend/prebuilt/assets/index-pnVde5Cl.js.map deleted file mode 100644 index 1d55c813..00000000 Binary files a/frontend/prebuilt/assets/index-pnVde5Cl.js.map and /dev/null differ diff --git a/frontend/prebuilt/assets/leaflet-By4Srz79.js b/frontend/prebuilt/assets/leaflet-ENrCR1B4.js similarity index 99% rename from frontend/prebuilt/assets/leaflet-By4Srz79.js rename to frontend/prebuilt/assets/leaflet-ENrCR1B4.js index 5168290c..e2a609dd 100644 Binary files a/frontend/prebuilt/assets/leaflet-By4Srz79.js and b/frontend/prebuilt/assets/leaflet-ENrCR1B4.js differ diff --git a/frontend/prebuilt/assets/leaflet-By4Srz79.js.map b/frontend/prebuilt/assets/leaflet-ENrCR1B4.js.map similarity index 99% rename from frontend/prebuilt/assets/leaflet-By4Srz79.js.map rename to frontend/prebuilt/assets/leaflet-ENrCR1B4.js.map index 6c07c40c..e9e2b2a6 100644 Binary files a/frontend/prebuilt/assets/leaflet-By4Srz79.js.map and b/frontend/prebuilt/assets/leaflet-ENrCR1B4.js.map differ diff --git a/frontend/prebuilt/assets/separator-CcV99KWD.js b/frontend/prebuilt/assets/separator-BwjFWJb_.js similarity index 89% rename from frontend/prebuilt/assets/separator-CcV99KWD.js rename to frontend/prebuilt/assets/separator-BwjFWJb_.js index d93cdbae..f6c43124 100644 Binary files a/frontend/prebuilt/assets/separator-CcV99KWD.js and b/frontend/prebuilt/assets/separator-BwjFWJb_.js differ diff --git a/frontend/prebuilt/assets/separator-CcV99KWD.js.map b/frontend/prebuilt/assets/separator-BwjFWJb_.js.map similarity index 98% rename from frontend/prebuilt/assets/separator-CcV99KWD.js.map rename to frontend/prebuilt/assets/separator-BwjFWJb_.js.map index e1af8a79..8ebcd5d6 100644 Binary files a/frontend/prebuilt/assets/separator-CcV99KWD.js.map and b/frontend/prebuilt/assets/separator-BwjFWJb_.js.map differ diff --git a/frontend/prebuilt/index.html b/frontend/prebuilt/index.html index a3f1ccfa..4ca2a682 100644 Binary files a/frontend/prebuilt/index.html and b/frontend/prebuilt/index.html differ diff --git a/frontend/src/components/MessageInput.tsx b/frontend/src/components/MessageInput.tsx index 49e6f4ad..bde9c441 100644 --- a/frontend/src/components/MessageInput.tsx +++ b/frontend/src/components/MessageInput.tsx @@ -24,6 +24,7 @@ const CHANNEL_WARNING_THRESHOLD = 120; // Conservative for multi-hop const CHANNEL_DANGER_BUFFER = 8; // Red zone starts this many bytes before hard limit const textEncoder = new TextEncoder(); +const RADIO_NO_RESPONSE_SNIPPET = 'no response was heard back'; /** Get UTF-8 byte length of a string (LoRa packets are byte-constrained, not character-constrained). */ function byteLen(s: string): number { return textEncoder.encode(s).length; @@ -118,8 +119,11 @@ export const MessageInput = forwardRef(fu setText(''); } catch (err) { console.error('Failed to send message:', err); - toast.error('Failed to send message', { - description: err instanceof Error ? err.message : 'Check radio connection', + const description = err instanceof Error ? err.message : 'Check radio connection'; + const isRadioNoResponse = + err instanceof Error && err.message.toLowerCase().includes(RADIO_NO_RESPONSE_SNIPPET); + toast.error(isRadioNoResponse ? 'Radio did not confirm send' : 'Failed to send message', { + description, }); return; } finally { diff --git a/frontend/src/test/messageInput.test.tsx b/frontend/src/test/messageInput.test.tsx index 8b156481..a5c4549c 100644 --- a/frontend/src/test/messageInput.test.tsx +++ b/frontend/src/test/messageInput.test.tsx @@ -9,12 +9,18 @@ import { render, screen, fireEvent } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { MessageInput } from '../components/MessageInput'; +import { toast } from '../components/ui/sonner'; // Mock sonner (toast) vi.mock('../components/ui/sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() }, })); +const mockToast = toast as unknown as { + success: ReturnType; + error: ReturnType; +}; + const textEncoder = new TextEncoder(); function byteLen(s: string): number { @@ -182,4 +188,24 @@ describe('MessageInput', () => { expect(getSendButton()).toBeEnabled(); }); }); + + describe('send failure toasts', () => { + it('shows the radio no-response toast when the send outcome is unknown', async () => { + onSend.mockRejectedValueOnce( + new Error( + 'Send command was issued to the radio, but no response was heard back. The message may or may not have sent successfully.' + ) + ); + renderInput({ conversationType: 'contact' }); + + fireEvent.change(getInput(), { target: { value: 'Hello' } }); + fireEvent.click(getSendButton()); + + expect(await screen.findByDisplayValue('Hello')).toBeTruthy(); + expect(mockToast.error).toHaveBeenCalledWith('Radio did not confirm send', { + description: + 'Send command was issued to the radio, but no response was heard back. The message may or may not have sent successfully.', + }); + }); + }); }); diff --git a/tests/test_send_messages.py b/tests/test_send_messages.py index 99d4a17d..1d479c33 100644 --- a/tests/test_send_messages.py +++ b/tests/test_send_messages.py @@ -25,6 +25,7 @@ from app.routers.messages import ( send_direct_message, ) from app.services import dm_ack_tracker +from app.services.message_send import NO_RADIO_RESPONSE_AFTER_SEND_DETAIL @pytest.fixture(autouse=True) @@ -1024,6 +1025,32 @@ class TestRadioExceptionMidSend: ) assert len(messages) == 0 + @pytest.mark.asyncio + async def test_dm_send_no_radio_response_returns_504_without_storing_message(self, test_db): + """When mc.commands.send_msg() returns None, report unknown outcome and store nothing.""" + mc = _make_mc() + pub_key = "ac" * 32 + await _insert_contact(pub_key, "Alice") + + mc.commands.send_msg = AsyncMock(return_value=None) + + with ( + patch("app.routers.messages.require_connected", return_value=mc), + patch.object(radio_manager, "_meshcore", mc), + pytest.raises(HTTPException) as exc_info, + ): + await send_direct_message( + SendDirectMessageRequest(destination=pub_key, text="Did this send?") + ) + + assert exc_info.value.status_code == 504 + assert exc_info.value.detail == NO_RADIO_RESPONSE_AFTER_SEND_DETAIL + + messages = await MessageRepository.get_all( + msg_type="PRIV", conversation_key=pub_key, limit=10 + ) + assert len(messages) == 0 + @pytest.mark.asyncio async def test_channel_send_radio_exception_no_orphan_message(self, test_db): """When mc.commands.send_chan_msg() raises, no message should be stored in DB."""