Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions src/renderer/components/MessageStatusBadge.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { render, screen } from '@testing-library/react';
import type { ReactElement } from 'react';
import { describe, expect, it, vi } from 'vitest';
import { axe } from 'vitest-axe';

import { MessageStatusBadge } from '@/renderer/components/MessageStatusBadge';
import { hydrateAxeThemeColors } from '@/renderer/lib/a11yTestHelpers';

vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}));

async function renderAndAssertAxe(ui: ReactElement): Promise<ReturnType<typeof render>> {
const view = render(ui);
hydrateAxeThemeColors(view.container);
expect(await axe(view.container)).toHaveNoViolations();
return view;
}

describe('MessageStatusBadge', () => {
it.each(['tcp', 'http'] as const)(
'uses WiFi mesh-ACK tooltip for failed device sends over %s',
async (connectionType) => {
await renderAndAssertAxe(
<MessageStatusBadge status="failed" transport="device" connectionType={connectionType} />,
);
expect(
screen.getByLabelText(
'messageStatusBadge.tooltipPrefixDevice: messageStatusBadge.noAckTooltipWifi',
),
).toBeTruthy();
expect(screen.getByText(/messageStatusBadge.transportWifi/)).toBeTruthy();
expect(screen.getByText(/messageStatusBadge.noAck/)).toBeTruthy();
},
);

it('keeps the generic no-ACK tooltip for failed serial device sends', async () => {
await renderAndAssertAxe(
<MessageStatusBadge status="failed" transport="device" connectionType="serial" />,
);
expect(
screen.getByLabelText(
'messageStatusBadge.tooltipPrefixDevice: messageStatusBadge.noAckTooltip',
),
).toBeTruthy();
expect(screen.queryByLabelText(/noAckTooltipWifi/)).toBeNull();
});
});
4 changes: 3 additions & 1 deletion src/renderer/components/MessageStatusBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ export function MessageStatusBadge({
status === 'failed' && context === 'room'
? (displayError ?? t('messageStatusBadge.failedPost'))
: status === 'failed' && transport === 'device'
? t('messageStatusBadge.noAckTooltip')
? connectionType === 'http' || connectionType === 'tcp'
? t('messageStatusBadge.noAckTooltipWifi')
: t('messageStatusBadge.noAckTooltip')
: displayError || t('messageStatusBadge.failed');
const tooltipPrefix =
transport === 'mqtt'
Expand Down
45 changes: 45 additions & 0 deletions src/renderer/lib/meshtastic/meshtasticSdkRoutingErrorLog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,51 @@ describe('meshtasticSdkRoutingErrorLog', () => {
expect(updateMessageStatus).toHaveBeenCalled();
});

it('does not fall back unmatched TIMEOUT onto a sole sending outbound', () => {
seedOutbound([{ packetId: 55, timestamp: Date.now() }]);
const applied = applyMeshtasticOutboundRoutingErrorFromLog(
'Packet 41841545 of type packet timed out',
{ myNodeNum: 42, identityId: IDENTITY },
);
expect(applied).toBe(false);
expect(updateMessageStatus).not.toHaveBeenCalled();
});

it('does not fall back unmatched MAX_RETRANSMIT onto a sole sending outbound', () => {
seedOutbound([{ packetId: 55, timestamp: Date.now() }]);
const applied = applyMeshtasticOutboundRoutingErrorFromLog(
'Error received for packet 985918657: MAX_RETRANSMIT',
{ myNodeNum: 42, identityId: IDENTITY },
);
expect(applied).toBe(false);
expect(updateMessageStatus).not.toHaveBeenCalled();
});

it('does not fall back unmatched NO_RESPONSE onto a sole sending outbound', () => {
seedOutbound([{ packetId: 55, timestamp: Date.now() }]);
const applied = applyMeshtasticOutboundRoutingErrorFromLog(
'Error received for packet 424242: NO_RESPONSE',
{ myNodeNum: 42, identityId: IDENTITY },
);
expect(applied).toBe(false);
expect(updateMessageStatus).not.toHaveBeenCalled();
});

it('still exact-matches MAX_RETRANSMIT when the wire id matches the outbound row', () => {
seedOutbound([{ packetId: 985918657, timestamp: Date.now() }]);
const applied = applyMeshtasticOutboundRoutingErrorFromLog(
'Error received for packet 985918657: MAX_RETRANSMIT',
{ myNodeNum: 42, identityId: IDENTITY },
);
expect(applied).toBe(true);
expect(updateMessageStatus).toHaveBeenCalledWith(
IDENTITY,
'985918657',
'failed',
'chatPanel.routingErrors.timeout',
);
});

it('does not apply when no outbound rows exist', () => {
seedOutbound([]);
const applied = applyMeshtasticOutboundRoutingErrorFromLog(
Expand Down
16 changes: 15 additions & 1 deletion src/renderer/lib/meshtastic/meshtasticSdkRoutingErrorLog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,15 @@ function storeOutboundMessagesAsChat(identityId: string, myNodeNum: number): Cha
return messageRecordsToChatMessages(records);
}

/** Errors that are unsafe to attribute via the single-sending fallback. */
function shouldAvoidSendingFallback(errorName: string): boolean {
return errorName === 'TIMEOUT' || errorName === 'NO_RESPONSE' || errorName === 'MAX_RETRANSMIT';
}

function findOutboundTargetForWirePacketId(
wirePacketId: number,
ctx: ApplyMeshtasticOutboundRoutingErrorContext,
errorName: string,
): ChatMessage | undefined {
const { myNodeNum, identityId, tempIdToWirePacketId } = ctx;
if (!identityId) return undefined;
Expand All @@ -171,6 +177,14 @@ function findOutboundTargetForWirePacketId(
// share-location Waypoint) — do not misattribute its NAK to a pending chat row.
if (hasMeshtasticNonChatOutboundInFlight()) return undefined;

// Unmatched TIMEOUT / MAX_RETRANSMIT / NO_RESPONSE must not steal the sole
// in-flight chat row (leftover queue TIMEOUTs were marking unrelated broadcasts
// failed). TransportManager still fails the matching sendText by tempId when
// the NAK belongs to that send.
if (shouldAvoidSendingFallback(errorName)) {
return undefined;
}

return findFallbackSendingOutbound(messages, myNodeNum);
}

Expand All @@ -196,7 +210,7 @@ export function applyMeshtasticOutboundRoutingError(
if (isMeshtasticNonChatWirePacketId(parsed.packetId)) {
return false;
}
const target = findOutboundTargetForWirePacketId(parsed.packetId, ctx);
const target = findOutboundTargetForWirePacketId(parsed.packetId, ctx, parsed.errorName);
if (!target || !identityId) {
return false;
}
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/locales/cs/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -2215,7 +2215,8 @@
"noAckTooltip": "Žádný ACK (zpráva mohla být stále vysílána; žádný jiný uzel v dosahu k potvrzení)",
"failed": "Nezdařilo se",
"tooltipPrefixMqtt": "MQTT",
"tooltipPrefixDevice": "Zařízení"
"tooltipPrefixDevice": "Zařízení",
"noAckTooltipWifi": "No mesh ACK — vaše WiFi/TCP spojení s rádiem je v pořádku. Zpráva mohla být stále odeslána; žádný jiný uzel LoRa nebyl potvrzen."
},
"modulePanel": {
"sectionSent": "{{name}} odesláno. Zařízení se může krátce restartovat.",
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/locales/de/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -2213,7 +2213,8 @@
"noAckTooltip": "Kein ACK (Nachricht darf noch gesendet worden sein; kein anderer Knoten in Reichweite zu quittieren)",
"failed": "Nicht bestanden",
"tooltipPrefixMqtt": "MQTT",
"tooltipPrefixDevice": "Vorrichtung"
"tooltipPrefixDevice": "Vorrichtung",
"noAckTooltipWifi": "Kein Mesh-ACK — Ihre WiFi/TCP-Verbindung zum Radio ist in Ordnung. Die Nachricht wurde möglicherweise noch gesendet; kein anderer LoRa-Knoten wurde bestätigt."
},
"modulePanel": {
"sectionSent": "{{name}} gesendet. Das Gerät kann kurzzeitig neu gestartet werden.",
Expand Down
1 change: 1 addition & 0 deletions src/renderer/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -2382,6 +2382,7 @@
"posted": "Posted",
"failedPost": "Failed to post",
"noAckTooltip": "No ACK (message may still have been broadcast; no other node in range to acknowledge)",
"noAckTooltipWifi": "No mesh ACK — your WiFi/TCP link to the radio is fine. Message may still have been sent; no other LoRa node acknowledged.",
"failed": "Failed",
"tooltipPrefixMqtt": "MQTT",
"tooltipPrefixDevice": "Device"
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/locales/es/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -2213,7 +2213,8 @@
"noAckTooltip": "Sin ACK (es posible que el mensaje aún se haya transmitido; no hay otro nodo en el rango para reconocer)",
"failed": "Error",
"tooltipPrefixMqtt": "MQTT",
"tooltipPrefixDevice": "Dispositivo"
"tooltipPrefixDevice": "Dispositivo",
"noAckTooltipWifi": "Sin ACK de malla: su enlace WiFi/TCP a la radio está bien. Es posible que aún se haya enviado el mensaje; no se ha reconocido ningún otro nodo LoRa."
},
"modulePanel": {
"sectionSent": "{{name}} enviado. El dispositivo puede reiniciarse brevemente.",
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/locales/fr/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -2213,7 +2213,8 @@
"noAckTooltip": "Aucun ACK (le message peut encore avoir été diffusé ; aucun autre nœud à portée pour accuser réception)",
"failed": "Échec",
"tooltipPrefixMqtt": "MQTT",
"tooltipPrefixDevice": "Dispositif"
"tooltipPrefixDevice": "Dispositif",
"noAckTooltipWifi": "Pas d'ACK maillé — votre connexion WiFi/TCP à la radio est correcte. Le message peut encore avoir été envoyé ; aucun autre nœud LoRa n'a été accusé de réception."
},
"modulePanel": {
"sectionSent": "{{name}} envoyé. L'appareil peut redémarrer brièvement.",
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/locales/id/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -2213,7 +2213,8 @@
"noAckTooltip": "Tidak ada ack (pesan mungkin masih disiarkan; tidak ada simpul lain dalam jangkauan untuk dikenali)",
"failed": "Gagal",
"tooltipPrefixMqtt": "MQTT",
"tooltipPrefixDevice": "Perangkat"
"tooltipPrefixDevice": "Perangkat",
"noAckTooltipWifi": "Tidak ada ack mesh — tautan WiFi/TCP Anda ke radio baik - baik saja. Pesan mungkin masih telah dikirim; tidak ada simpul LoRa lain yang diakui."
},
"modulePanel": {
"sectionSent": "{{name}} terkirim. Perangkat mungkin akan reboot sebentar.",
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/locales/it/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -2213,7 +2213,8 @@
"noAckTooltip": "Nessun ACK (il messaggio potrebbe essere ancora stato trasmesso; nessun altro nodo nel raggio d'azione da riconoscere)",
"failed": "Non riuscito",
"tooltipPrefixMqtt": "MQTT",
"tooltipPrefixDevice": "Dispositivo"
"tooltipPrefixDevice": "Dispositivo",
"noAckTooltipWifi": "Nessun ACK mesh: il tuo collegamento WiFi/TCP alla radio va bene. Il messaggio potrebbe essere stato ancora inviato; nessun altro nodo LoRa riconosciuto."
},
"modulePanel": {
"sectionSent": "{{name}} inviato. Il dispositivo potrebbe riavviarsi brevemente.",
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/locales/ja/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -2213,7 +2213,8 @@
"noAckTooltip": "ACKなし(メッセージはまだブロードキャストされている可能性があります。確認応答する範囲内の他のノードはありません)",
"failed": "失敗しました",
"tooltipPrefixMqtt": "MQTT",
"tooltipPrefixDevice": "デバイス"
"tooltipPrefixDevice": "デバイス",
"noAckTooltipWifi": "メッシュACKなし—無線へのWi - Fi/TCPリンクは問題ありません。メッセージはまだ送信されている可能性があります。他のLoRaノードは確認応答していません。"
},
"modulePanel": {
"sectionSent": "{{name}} が送信されました。デバイスが短時間再起動する場合があります。",
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/locales/ko/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -2213,7 +2213,8 @@
"noAckTooltip": "ACK 없음 (메시지는 여전히 브로드캐스트되었을 수 있으며, 확인할 수 있는 범위에 있는 다른 노드는 없음)",
"failed": "실패함",
"tooltipPrefixMqtt": "MQTT",
"tooltipPrefixDevice": "장치"
"tooltipPrefixDevice": "장치",
"noAckTooltipWifi": "메시 ACK 없음 — 라디오에 대한 WiFi/TCP 링크는 괜찮습니다. 메시지가 여전히 전송되었을 수 있습니다. 다른 LoRa 노드가 확인되지 않았습니다."
},
"modulePanel": {
"sectionSent": "{{name}} 보냈습니다. 장치가 잠시 재부팅될 수 있습니다.",
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/locales/nl/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -2213,7 +2213,8 @@
"noAckTooltip": "Geen ACK (bericht kan nog steeds zijn uitgezonden; geen ander knooppunt binnen bereik om te bevestigen)",
"failed": "Mislukt",
"tooltipPrefixMqtt": "MQTT",
"tooltipPrefixDevice": "Toestel"
"tooltipPrefixDevice": "Toestel",
"noAckTooltipWifi": "Geen mesh ACK — uw WiFi/TCP-link naar de radio is prima. Mogelijk is er nog steeds een bericht verzonden; er is geen ander LoRa-knooppunt bevestigd."
},
"modulePanel": {
"sectionSent": "{{name}} verzonden. Het apparaat kan kortstondig opnieuw opstarten.",
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/locales/pl/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -2217,7 +2217,8 @@
"noAckTooltip": "Brak ACK (komunikat mógł nadal zostać wyemitowany; brak innego węzła w zasięgu do potwierdzenia)",
"failed": "Zakończone niepowodzeniem",
"tooltipPrefixMqtt": "MQTT",
"tooltipPrefixDevice": "Urządzenie"
"tooltipPrefixDevice": "Urządzenie",
"noAckTooltipWifi": "Brak ACK siatki — Twoje łącze WiFi/TCP do radia jest w porządku. Wiadomość mogła nadal zostać wysłana; żaden inny węzeł LoRa nie został potwierdzony."
},
"modulePanel": {
"sectionSent": "Wysłano: {{name}}. Urządzenie może się na chwilę zrestartować.",
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/locales/pt-BR/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -2213,7 +2213,8 @@
"noAckTooltip": "Sem ACK (a mensagem ainda pode ter sido transmitida; nenhum outro nó no intervalo para confirmar)",
"failed": "Falha",
"tooltipPrefixMqtt": "MQTT",
"tooltipPrefixDevice": "Dispositivo"
"tooltipPrefixDevice": "Dispositivo",
"noAckTooltipWifi": "Sem mesh ACK — seu link WiFi/TCP para o rádio está bom. A mensagem ainda pode ter sido enviada; nenhum outro nó LoRa foi reconhecido."
},
"modulePanel": {
"sectionSent": "{{name}} enviado. O dispositivo pode reiniciar brevemente.",
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/locales/ru/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -2215,7 +2215,8 @@
"noAckTooltip": "Нет ACK (сообщение, возможно, все еще было передано; нет другого узла в диапазоне для подтверждения)",
"failed": "Не удалось",
"tooltipPrefixMqtt": "MQTT",
"tooltipPrefixDevice": "Устройство"
"tooltipPrefixDevice": "Устройство",
"noAckTooltipWifi": "Нет Mesh ACK — ваша связь WiFi/TCP с радиостанцией в порядке. Сообщение, возможно, все еще было отправлено; ни один другой узел LoRa не подтвержден."
},
"modulePanel": {
"sectionSent": "{{name}} отправлено. Устройство может ненадолго перезагрузиться.",
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/locales/tr/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -2213,7 +2213,8 @@
"noAckTooltip": "ACK yok (mesaj hala yayınlanmış olabilir; onaylanacak başka bir düğüm yok)",
"failed": "Başarısız",
"tooltipPrefixMqtt": "MQTT",
"tooltipPrefixDevice": "Cihaz"
"tooltipPrefixDevice": "Cihaz",
"noAckTooltipWifi": "Ağ ACK'si yok; telsize WiFi/TCP bağlantınız iyi durumda. Mesaj hala gönderilmiş olabilir; başka hiçbir LoRa düğümü onaylanmadı."
},
"modulePanel": {
"sectionSent": "{{name}} gönderildi. Cihaz kısa süreliğine yeniden başlatılabilir.",
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/locales/uk/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -2215,7 +2215,8 @@
"noAckTooltip": "Немає ACK (повідомлення, можливо, все ще транслювалося; немає іншого вузла в діапазоні для підтвердження)",
"failed": "Помилка",
"tooltipPrefixMqtt": "MQTT",
"tooltipPrefixDevice": "Пристрій"
"tooltipPrefixDevice": "Пристрій",
"noAckTooltipWifi": "Без Mesh ACK — ваше посилання WiFi/TCP на радіо в порядку. Можливо, повідомлення все ще було надіслано; жоден інший вузол LoRa не підтверджено."
},
"modulePanel": {
"sectionSent": "{{name}} надіслано. Пристрій може ненадовго перезавантажитися.",
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/locales/zh/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -2213,7 +2213,8 @@
"noAckTooltip": "无确认(消息可能仍已广播;范围内没有其他节点需要确认)",
"failed": "失败",
"tooltipPrefixMqtt": "MQTT",
"tooltipPrefixDevice": "设备"
"tooltipPrefixDevice": "设备",
"noAckTooltipWifi": "无网状确认—您可以使用无线网络/TCP连接到收音机。消息可能仍已发送;未确认其他LoRa节点。"
},
"modulePanel": {
"sectionSent": "{{name}}已发送。设备可能会短暂重新启动。",
Expand Down