diff --git a/AGENTS.md b/AGENTS.md index b46b0147f..bcafc8c3c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -149,7 +149,7 @@ Conventional Commits (`feat:`, `fix:`, `docs:`, `chore:`, `refactor:`, `test:`). - **RNode flasher timeouts:** `RNODE_COMMAND_TIMEOUT_MS` (30 s serial), `RNODE_BT_PAIRING_TIMEOUT_MS` (90 s BLE pairing), `ESP32_FLASH_STALL_TIMEOUT_MS` / `NRF52_DFU_STALL_TIMEOUT_MS` (60 s no-progress → `ESP32_FLASH_STALLED` / `NRF52_DFU_STALLED`); humanized via `flasherErrorHumanize.ts` - **Peer aliases:** LXMF/Nomad announce names overlay path-table **peers and contacts** (`list_contacts` fills nameless/hash-prefix rows from announce/peer/Nomad cache and may persist; upsert rejects hash-prefix placeholders); renderer refresh + `reticulumContactToNodeRecordPreservingLabel` refuse hash-prefix wipes of Chat/`nodeStore` labels; ingest omits hash-prefix placeholder `sender_name`; SQL upsert guard in `db:upsertReticulumDestination` preserves real names over hash-prefix aliases; destination upsert requires exact 32-hex (lowercase) and omits `favorited` on icon-only patches so favorites/icons survive path/probe refresh - **Stores/lib:** `reticulumIdentityStore.ts` (session-global sidecar identity status shared by `useReticulumSidecarApi` — distinct from identity-scoped `identityStore`), `reticulumPeerStore.ts` (path-table peers + LXMF contacts; soft-TTL reads, forced `?refresh=1`, incremental `peers_updated` route-field patches, 50ms batching, name/appearance preservation, 30s/60s large-mesh poll), `reticulumDiscoveryMapStore.ts`, `reticulumRmapDiscovery.ts`, `reticulumDiscoveryMapLayout.ts`, `nomadNetworkStore.ts`, `rrcHubStore.ts` / `rrcSessionStore.ts` (RRC hubs + multi-hub sessions; room history → SQLite `rrc_messages` via `rrcMessagePersist.ts` + `ipc/rrc-db-handlers.ts`; prefs in `rrcHubPrefs` / `rrcRoomPrefs` / `rrcRecentRooms`; notifications in `rrcInactiveNotifications` / `rrcMention`); **Remote (rnsh/rncp):** `rncpTransferStore.ts`, `rnshSessionStore.ts`, `reticulumInboundPolicyStore.ts`, `reticulumRemoteAddressStore.ts`, `rncpEnableRequestStore.ts` + lib `remoteSettingsStorage.ts`, `pushRncpListenerPolicy.ts`, `rncpInboundPolicyLists.ts`, `sendRncpRequestEnable.ts`, `rncpRequestEnableRateLimit.ts`, `hooks/useRemotePathCapability.ts`, `components/remote/*`; WS events `rmap.discovery`, `lxmf_outbound_status`, `nomadnetwork.node`, `rrc.*`, `rnsh.*` / `rncp.*` in `useReticulumRuntime` (sidecar also emits `nomad.serving_start` / `nomad.serving_stop`; renderer polls serving status via HTTP, not those WS events) -- **LXMF outbound delivery:** sidecar `lxmf_delivery.rs` / `lxmf_outbound.rs`; renderer `applyReticulumOutboundDeliveryStatus.ts` (WS `lxmf_outbound_status` → Zustand + SQLite `delivery_status`; early-status buffer), `reticulumOutboundFailureBridge.ts`, `markStaleReticulumOutbound.ts` +- **LXMF outbound delivery:** sidecar `lxmf_delivery.rs` / `lxmf_outbound.rs` (Direct-first; **one-shot fallback** to preferred **remote** PN on Direct fail; intermediate WS `sending` + `delivery_method: "propagated"`); renderer `applyReticulumOutboundDeliveryStatus.ts` (WS `lxmf_outbound_status` → Zustand + SQLite `delivery_status` + `delivery_method`; early-status buffer; hash/status allowlist), `reticulumOutboundFailureBridge.ts` (skips `propagated` rows so link-timeout does not kill PN fallback), `markStaleReticulumOutbound.ts`. Propagated Completes UI: **Stored at propagation node**. - **DM path reachability:** `useReticulumDmPathProbe.ts`, `reticulumDmPathReachability.ts`, `ReticulumDmPathReachabilityBadge.tsx` — Chat **Probe** matches Peer List (sidecar running check → `/probe` → toast → refresh); `applyProbeResult(forHash, …)` applies the settle without a second `/probe` and ignores stale completions after DM switch; manual reprobe forces Checking… even when passive hops look reachable; Peers virtualizes above 100 rows via `reticulumPeerListRows.ts`; peer refresh policy in `reticulumSidecarPeerRefreshEvents.ts` - **Inbound transport labels:** `received_via` resolves the path-table interface name against local interface config type, so a TCP hub display name still renders as TCP. - **Topology:** `via_hash` is an immediate transport id; sidecar synthesizes missing relay nodes. `ReticulumTopologyPanel` uses force layout; sidecar caps graph input at 2,000 peers and renderer caps visible peers at 800 (grid repulsion above 400). @@ -159,14 +159,14 @@ Conventional Commits (`feat:`, `fix:`, `docs:`, `chore:`, `refactor:`, `test:`). - **Gating:** `hasReticulumDiscoveryMap` (Map tab); `hasReticulumRemotePanel` / `hasRncpTransfer` (Remote tab + Chat DM rncp); `hasRrcPanel` (RRC tab); `hasReticulumInterfaceConfig` / `hasReticulumNetworkPanel` / `ProtocolCapabilities` - **rnsh/rncp:** sidecar `stack/{rnsh_session,rncp_transfer,path_speed,link_task}.rs` + HTTP `/api/v1/rnsh/*`, `/api/v1/rncp/*`, `/api/v1/remote/*`; typed `electronAPI.reticulum.rnsh|rncp|remote`; picker-gated send/fetch paths in `reticulum-remote-paths.ts`; LXMF enable-request sentinel `mesh-client:request-rncp-receive:v1` (`rncpRequestEnable.ts`); inbound listener config persists (`rncp_listener_*` in `mesh_client_stack.json`) and restores on live stack start - **Runtime:** `useReticulumRuntime`, `lib/sessions/reticulumSession.ts`, `lib/ingest/reticulumIngest.ts`; connect starts sidecar, not `ConnectionDriver` RF. Sidecar RRC: `rrc_codec` / `rrc_link` / `rrc_session` / `api/rrc.rs` -- **Diagnostics:** `ReticulumDiagnosticEngine.ts` (Reticulum-native rows; no LoRa hop-goblin semantics) +- **Diagnostics:** `ReticulumDiagnosticEngine.ts` (Reticulum-native rows; no LoRa hop-goblin semantics) — includes `reticulum/sidecar-unhealthy` (60s grace), `reticulum/propagation-sync-stuck`, `reticulum/propagation-sync-failing` (1h TTL) - **No Noble/MQTT** for Reticulum's own connections (sidecar owns BLE RNode via `btleplug`); gate UI with `hasReticulumInterfaceConfig` / `hasReticulumNetworkPanel` / `ProtocolCapabilities`. On macOS/Windows, connecting a Reticulum BLE RNode may still **suspend/yield Noble** so it does not contend with the sidecar's BLE scan — see **Multi-protocol BLE** below. - **Multi-protocol BLE:** Meshtastic, MeshCore, and Reticulum (BLE Peer + `ble://` RNode) may connect to **different** BLE devices at once on all platforms. Coexistence: `ble-coexistence-coordinator.ts` (peripheral MAC registry + scan-only mutex); Linux mesh uses Web Bluetooth + sidecar `btleplug`. Same MAC rejected; scans serialized—never disconnect unrelated GATT for scans. **Reticulum BLE RNode** on macOS/Windows may **suspend Noble** (`suspendNobleForReticulumBleConnect`, `reticulum-ble-rnode-config.ts`, `reticulumNobleBleYield.ts`, `useReticulumNobleBleYieldWatcher`); while yield holds the scan, Noble connect is rejected; post-grace yield stops re-contending (**~60s** grace aligns with OS passkey window); disconnect timeout fails closed (releases scan). Sidecar may latch **`bleBondRemoved`** (stale OS bonds) or **`blePairingTimedOut`** (passkey not entered) — Forget/re-pair; Admin Start pairing shows PIN in-panel over USB (not radio display; never Meshtastic `123456`). Release dispatches `mesh-client:nobleBleYieldReleased` for Meshtastic/MeshCore reconnect. - **Docs:** [docs/reticulum.md](docs/reticulum.md), [docs/reticulum-sidecar-ipc.md](docs/reticulum-sidecar-ipc.md) ### Diagnostics -- **Engines:** `src/renderer/lib/diagnostics/`; `RoutingDiagnosticEngine.ts`, `RFDiagnosticEngine.ts`, `RemediationEngine.ts`. +- **Engines:** `src/renderer/lib/diagnostics/`; `RoutingDiagnosticEngine.ts`, `RFDiagnosticEngine.ts` (includes MeshCore **High Companion TX Queue** when `queueLen > 200`), `RemediationEngine.ts`, `ReticulumDiagnosticEngine.ts`. - **Store:** `src/renderer/stores/diagnosticsStore.ts`; routing/RF rows, foreign LoRa, MQTT ignore, redundancy. - **Tab scoping:** `filterDiagnosticRowsForProtocol()` — Meshtastic/MeshCore tabs show LoRa rows only; Reticulum tab shows `reticulum/*` only. Foreign-LoRa tables UI is Meshtastic-tab-only. - **Extend:** adjust `DiagnosticRow` in `src/renderer/lib/types.ts`, add detector, wire `replaceRoutingRowsFromMap` / `replaceRfRowsForNode`; TTL defaults in `diagnosticRows.ts` (routing 24h, RF 1h). diff --git a/README.md b/README.md index 3a72f2a27..58638b5c9 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ Mesh-Client supports **three mesh stacks** in one desktop app. Use the header ** **Network Diagnostics** -- **Protocol-scoped rows**: The shared Diagnostics tab filters findings to the **active protocol** — Meshtastic/MeshCore show LoRa routing + RF rows only; Reticulum shows `reticulum/*` interface/path/LXMF rows only (Reticulum interface-down alerts no longer bleed onto LoRa tabs). Map halos, node-list badges, and node detail routing sections use the same filter. +- **Protocol-scoped rows**: The shared Diagnostics tab filters findings to the **active protocol** — Meshtastic/MeshCore show LoRa routing + RF rows only (including MeshCore **High Companion TX Queue**); Reticulum shows `reticulum/*` interface/path/LXMF rows only (sidecar health, propagation sync stuck/failing, interface-down alerts no longer bleed onto LoRa tabs). Map halos, node-list badges, and node detail routing sections use the same filter. - **Network health**: status band **Healthy / Attention / Degraded** plus error and warning counts. **Degraded** applies only when routing error count ≥ 3; fewer errors use **Attention** so small issues don't paint the whole panel red - **Single table** from `diagnosticRows` (routing trace rows + RF rows), searchable; rows persist across sessions with an optional restore banner; **max age** (1–168 hours) trims stale routing (24 h default) and RF (1 h default) rows - **Foreign LoRa overhear** (Meshtastic tab): MeshCore-heard, Reticulum RNS, other-Meshtastic, and unknown LoRa classes from decode-fail logs and dual-radio RX; 90-minute window; MeshCore repeater conflict escalation above 5 pkt/min @@ -322,7 +322,7 @@ Reticulum is the third protocol tab (**amber** pill). The stack runs in an **AGP - **Chat tab:** **DM-only** LXMF text and reactions (peer file transfer via Remote rncp; historic LXMF attachment labels still render read-only) - **RRC tab:** multi-hub relay chat (rooms, nicklists, slash commands, favourites, auto-join, reconnect; up to 8 hubs) - **Remote tab:** **rnsh** interactive shell sessions and **rncp** file transfer (send / receive / fetch) over Reticulum, with saved addresses and inbound-policy controls; also available from Chat DMs as send-file (distinct from Meshtastic remote admin) -- **Direct** delivery when the destination is in the path table; **propagated (PN)** handoff when offline and a propagation node is configured +- **Direct** delivery when the destination is in the path table (then **one-shot remote PN fallback** on Direct fail when a preferred remote PN is set); **propagated (PN)** when offline — Completes show **Stored at propagation node**, not recipient Delivered **Peers, topology, Nomad Network** @@ -365,7 +365,7 @@ Reticulum is the third protocol tab (**amber** pill). The stack runs in an **AGP - **Map tiles; OpenStreetMap Referer requirement**: Packaged desktop builds load the UI from the local filesystem. The main process now loads the renderer with an explicit HTTP referrer so OpenStreetMap tile requests include a valid `Referer` header and comply with the [tile usage policy](https://operations.osmfoundation.org/policies/tiles/). If you point the app at a different tile server, ensure its usage policy permits this client. - **Reticulum — no LoRa companion parity**: Reticulum does not use Meshtastic/MeshCore `ConnectionDriver`, MQTT hybrid, channel pills, Rooms BBS, or Hop Goblins diagnostics. The **Chat** tab is **DM-only**; hub room chat lives on the **RRC** tab. Interface add/edit/delete updates config on disk — **restart the stack** after changes under `rns-stack`. - **Reticulum — sidecar license**: The spawned `mesh-client-reticulum` binary is **AGPL-3.0** (separate process from the MIT Electron shell). See [docs/reticulum.md](docs/reticulum.md) and [docs/credits.md](docs/credits.md#bundled-binaries). -- **Reticulum — propagation required for offline peers**: LXMF send fails with `no_propagation_node` when the destination is not in the path table and no preferred propagation node is set. +- **Reticulum — propagation required for offline peers**: LXMF send fails with `no_propagation_node` when the destination is not in the path table and no preferred **remote** propagation node is set. Local inbox ≠ remote store-and-forward. When a path exists, Direct is tried first; Direct fail with a remote preferred PN triggers one-shot PN deposit. --- diff --git a/docs/diagnostics.md b/docs/diagnostics.md index a40c74782..509b269f8 100644 --- a/docs/diagnostics.md +++ b/docs/diagnostics.md @@ -167,10 +167,11 @@ These findings use telemetry observed from other nodes' radio stats. These findings use packet-stats data from a MeshCore device's Repeater Status response (`meshcore_local_stats`). -| Finding | Trigger | Severity | Meaning | -| -------------------- | ------------------------------------------------------------------------------ | -------- | --------------------------------------------------------------------------------------------- | -| Elevated Noise Floor | Radio noise floor > −95 dBm | Warning | Elevated interference from nearby RF sources reducing effective range | -| Excessive Flooding | ≥ 20 total transmissions and > 90% are flood-routed (`nSentFlood / totalSent`) | Warning | Direct routing has not been established with nearby nodes; all traffic falls back to flooding | +| Finding | Trigger | Severity | Meaning | +| ----------------------- | ------------------------------------------------------------------------------ | --------------- | --------------------------------------------------------------------------------------------- | +| Elevated Noise Floor | Radio noise floor > −95 dBm | Warning | Elevated interference from nearby RF sources reducing effective range | +| Excessive Flooding | ≥ 20 total transmissions and > 90% are flood-routed (`nSentFlood / totalSent`) | Warning | Direct routing has not been established with nearby nodes; all traffic falls back to flooding | +| High Companion TX Queue | `meshcore_local_stats.queueLen > 200` (error if `> 250`) | Warning / Error | Companion outbound queue near the 256 cap; traffic may be delayed or dropped | --- @@ -430,6 +431,14 @@ On the **Reticulum** protocol tab, the **Diagnostics** panel includes a **Reticu Runtime interface-issue rows from the sidecar latch (`interfaceIssueAlert`) are also folded into Diagnostics via `ReticulumDiagnosticEngine` — including TCP connect failures, TX queue drops, link-delivery timeouts, transport saturation, **`bleBondRemoved`** (stale OS Bluetooth bond for an RNode; Forget/re-pair), and **`blePairingTimedOut`** (OS passkey not entered within the TX-read window). +Additional runtime rows (refreshed from sidecar status + `reticulumPropagationStore`, not only the config audit poll): + +| Condition | Trigger | Severity | Action | +| ------------------------------------ | --------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------- | +| `reticulum/sidecar-unhealthy` | Sidecar `running && healthy === false` for ≥ 60 s (`sidecarUnhealthySince`) | error | **Restart stack** | +| `reticulum/propagation-sync-stuck` | Sync active ≥ ~45 s (`RETICULUM_PROPAGATION_SYNC_STALL_MS`) with progress still Establishing (< 15) | warning | Retry sync; check PN path / announce | +| `reticulum/propagation-sync-failing` | Sync idle with `lastSyncError` (excludes user cancel) and attempt within 1 h | warning | See Network → Propagation / troubleshooting | + | Issue kind | Typical cause | In-app action | | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | | `tcp_enable_key` / `ghost_interface` | TCP blocks use `enabled = Yes` but RNS only loads `interface_enabled` | **Repair config** | diff --git a/docs/reticulum-sidecar-ipc.md b/docs/reticulum-sidecar-ipc.md index 154953151..3416221bb 100644 --- a/docs/reticulum-sidecar-ipc.md +++ b/docs/reticulum-sidecar-ipc.md @@ -196,7 +196,7 @@ Event types: `lxmf_message`, `lxmf_outbound_status`, `announce.received`, `peers - **`rrc.disconnected`:** payload `{ hub_dest_hash, reason, will_reconnect? }`. When `will_reconnect` is `false` (or `reason` is `local_disconnect`), the renderer drops that hub session. When `true` (or omitted on older sidecars), the UI shows reconnecting and keeps volatile rooms until WELCOME. -- **`lxmf_outbound_status`:** authoritative outbound delivery updates. Payload: `{ message_hash, status, delivery_method?, to_hash? }` where `status` is `delivered` or `failed` (intermediate states are not emitted on WS). mesh-client maps `delivered` → UI Completes (`acked`) and persists `delivery_status` to SQLite; `failed` → Failed. Do **not** treat `/api/v1/lxmf/send` response `delivery_status` (`queued`/`sending`) as terminal. +- **`lxmf_outbound_status`:** authoritative outbound delivery updates. Payload: `{ message_hash, status, delivery_method?, to_hash?, sent_via? }` where `status` is `delivered`, `failed`, or intermediate `sending` (egress upgrade or Direct→PN fallback). mesh-client maps `delivered` → UI Completes (`acked`) and persists `delivery_status` (+ `delivery_method` when present) to SQLite; Propagated Completes show **Stored at propagation node**; `failed` → Failed. Do **not** treat `/api/v1/lxmf/send` response `delivery_status` (`queued`/`sending`) as terminal. After Direct failure with a preferred remote PN, the sidecar re-queues once as Propagated and emits `sending` + `delivery_method: "propagated"` before a final `delivered`/`failed`. - **`announce.received`:** emitted for every LXMF identity announce / path response the sidecar observes (named or nameless). Payload: `{ destination_hash, display_name?, hops }`. Display names update the peer-label cache only — announces do **not** auto-create LXMF contacts. That cache is overlayed onto `GET /api/v1/peers` / topology rows **and** onto nameless/hash-prefix rows from `GET /api/v1/contacts` (`list_contacts` may persist those fills) so path-table and contact refreshes keep announce aliases. - **`peers_updated`:** also emitted when the live path table **gains** new destination hashes (maintenance tick). Payload may include `{ added: string[], patches: PeerRow[], count }` (added/patches capped at 1024). Renderer applies patches incrementally, including route-field changes. A full peer dump is used on connect, manual Refresh, restart, safety poll, or a `peers_updated` payload that cannot be applied incrementally: `cleared`, `demoted_from_contacts`, or a single-`hash` probe/path event. Hop/timestamp-only churn does not emit. diff --git a/docs/reticulum.md b/docs/reticulum.md index 975b42a5f..932e8d012 100644 --- a/docs/reticulum.md +++ b/docs/reticulum.md @@ -32,7 +32,7 @@ After changing interfaces on a live network, **restart the stack** so RNS picks | LXMF chat | DM-only text and reactions (outbound LXMF file/voice attach removed; inbound attachment labels still render; use Remote rncp for peer files) | | Remote | **rnsh** multi-session shell + **rncp** send/receive/fetch under one tab (Shell / Transfer / Saved / Settings); Chat DM send-file convenience; path-speed gate (TCP/network); inbound Ask/allow-list; auto-reconnect / auto-retry; LXMF “request enable receive” prompt between mesh-client peers | | RRC | Reticulum Relay Chat — discovered/manual/favourite hubs, up to **8** concurrent sessions, hub/room auto-join, rooms, nicklists, slash commands (`/list`, `/who`, `/join`, …), @mention unread badges, toasts when the RRC tab is inactive, automatic reconnect with backoff | -| Delivery | **Direct** when destination is in path table, **Propagated (PN)** when offline and a preferred propagation node is set; path presence and transport badges (RF/BLE/TCP/NET, explicit multi like RF+TCP, PN) indicate **observed / path-table egress**, not “interfaces enabled” and not final LXMF delivery — UI stays **Sending** until `lxmf_outbound_status` (`delivered` / `failed`); terminal status persists in SQLite. Inbound `received_via` / TCP badges use the matching local interface **config type**, not its display name, so a hub named “RNS Testnet” is TCP. | +| Delivery | **Direct** when destination is in path table (then **one-shot fallback** to preferred **remote** PN on Direct fail); **Propagated (PN)** when offline and a preferred remote PN is set. Path/transport badges (RF/BLE/TCP/NET, multi, PN) are egress evidence — UI stays **Sending** until `lxmf_outbound_status` (`delivered` / `failed`); Propagated Completes show **Stored at propagation node**. Terminal `delivery_status` + `delivery_method` persist in SQLite. Local inbox ≠ remote store-and-forward. Inbound `received_via` / TCP badges use local interface **config type**, not display name. | | Peers | RNS path table + LXMF contacts (Peers tab sub-tabs); probe and peer detail modal | | Topology | Best-effort graph from path-table next hops (not a full multi-hop trace) | | Map | Local RMAP v4 discovery map (heard opt-in interfaces with GPS); link to rmap.world for global view | @@ -242,8 +242,9 @@ When multiple enabled local RNode interfaces are connected, the interface list s - **DM-only** on the Chat tab — no RF channel pills (RRC covers hub rooms separately) - Text and emoji reactions. **Outbound LXMF file/voice attach is not offered** (removed); historic `[file:name:mime]` bubbles and inbound Sideband-style attachments still render as a read-only label. Peer file transfer is via Remote rncp. - **Replies:** outbound DMs stamp LXMF `FIELD_REPLY_TO` (0x30) and optional `FIELD_REPLY_QUOTE` (0x31, capped) before sign so peers see structured replies; ingest/Chat use `reticulum_reply_to_hash` plus quote preview (store parent when present, else wire quote) and jump-to-parent by message hash -- Outbound **Sending** until sidecar emits `lxmf_outbound_status` (`delivered` / `failed`); `/api/v1/lxmf/send` may return `delivery_status: "queued"` or `"sending"` — that is enqueue/acceptance, not delivery confirmation -- Terminal **Completes** / **Failed** from `lxmf_outbound_status` are persisted to SQLite (`delivery_status` on `reticulum_messages`) via `applyReticulumOutboundDeliveryStatus.ts` so restart/DB hydration does not regress delivered rows; early WS events before provisional id→hash rekey are buffered +- Outbound **Sending** until sidecar emits `lxmf_outbound_status` (`delivered` / `failed`); `/api/v1/lxmf/send` may return `delivery_status: "queued"` or `"sending"` — that is enqueue/acceptance, not delivery confirmation. On Direct failure with a preferred **remote** PN, the sidecar re-emits `sending` with `delivery_method: "propagated"` for the one-shot PN deposit +- Terminal **Completes** / **Failed** from `lxmf_outbound_status` are persisted to SQLite (`delivery_status` + `delivery_method` on `reticulum_messages`) via `applyReticulumOutboundDeliveryStatus.ts` so restart/DB hydration keeps PN vs Direct labeling; early WS events before provisional id→hash rekey are buffered +- Propagated Completes render as **Stored at propagation node** (PN badge) — not recipient **Delivered** - **DM path reachability:** active DM header shows a reachability badge (`ReticulumDmPathReachabilityBadge` + `useReticulumDmPathProbe`) seeded from path-table/contact hops, then settled by peer probe; when settled, **Request path** / **Probe** use the same sidecar endpoints as the Peers tab. Chat **Probe** mirrors Peer List UX: stack-running check → `/probe` → toast → peer refresh; `onProbeSettled` / `applyProbeResult(forHash, …)` applies the result without a second `/probe` (stale hashes after DM switch are ignored); manual reprobe forces Checking… even when passive hops already look reachable ## RRC (Reticulum Relay Chat) @@ -258,13 +259,14 @@ IRC-style multi-pane client (`RrcPanel` + `rrcHubStore` / `rrcSessionStore`): ### Delivery modes -| Path table | Propagation node | Routing / UI | -| ------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Destination present | — | **Direct** link delivery; RF/BLE/TCP/NET (or explicit multi e.g. RF+TCP) badge = path-table / PacketTap egress — message stays **Sending** until `lxmf_outbound_status: delivered` | -| Destination absent | Preferred PN set | **Propagated** via preferred propagation node; **PN** badge = store-and-forward path — still **Sending** until sidecar confirms | -| Destination absent | None | Error `no_propagation_node`; set preferred node on Network tab | +| Path table | Propagation node | Routing / UI | +| ------------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Destination present | — | **Direct** link delivery; RF/BLE/TCP/NET (or explicit multi e.g. RF+TCP) badge = path-table / PacketTap egress — message stays **Sending** until `lxmf_outbound_status: delivered` | +| Destination present | Preferred **remote** PN set | Same Direct-first attempt; if Direct **fails**, sidecar **one-shot retries via preferred remote PN** (not Local inbox). UI switches to **PN** / **Stored at propagation node** on PN Complete | +| Destination absent | Preferred PN set | **Propagated** via preferred propagation node; **PN** badge = store-and-forward — Completes as **Stored at propagation node** (not recipient-delivered) | +| Destination absent | None | Error `no_propagation_node`; set preferred **remote** node on Network tab | -**Path ≠ delivered:** a path-table entry means RNS knows a route, not that LXMF completed. Reticulum is async — offline peers need a propagation node, not a TCP-style immediate refusal. +**Path ≠ delivered:** a path-table entry means RNS knows a route, not that LXMF completed. Reticulum is async — offline peers need a **remote** propagation node. **Local propagation (offline inbox)** is this device’s inbox / optional local serving — it does **not** deposit outbound DMs for unreachable peers. Propagated Completes mean the PN accepted the encrypted blob (Ratspeak envelope parity), not that the recipient opened Chat. --- @@ -313,10 +315,10 @@ Firmware `.zip` files are selected locally (no in-app GitHub download). Disconne ### SQLite (main process) -| Table | Contents | -| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `reticulum_destinations` | Contact rows (canonical 32-hex hash, display name, favorited, `icon_name`, `icon_color`) | -| `reticulum_messages` | LXMF history (`message_hash`, `reply_to_hash`, `delivery_status` — `sending`/`queued`/`pending`/`delivered`/`failed`; terminal outbound status written on `lxmf_outbound_status`; stale `sending` rows marked failed on startup) | +| Table | Contents | +| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `reticulum_destinations` | Contact rows (canonical 32-hex hash, display name, favorited, `icon_name`, `icon_color`) | +| `reticulum_messages` | LXMF history (`message_hash`, `reply_to_hash`, `delivery_status` — `sending`/`queued`/`pending`/`delivered`/`failed`; `delivery_method` — `direct`/`propagated`/`opportunistic`; terminal outbound status written on `lxmf_outbound_status`; stale `sending` rows marked failed on startup) | ### Sidecar `userData` diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 91609d410..6ca3f3f61 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1135,7 +1135,7 @@ Unrecognized codes pass through unchanged. 1. **Shared instance conflict** — `share_instance = Yes` with another Reticulum app still running (MeshChatX, Ratspeak, standalone `rnsd`) fighting the same IPC socket. mesh-client may attach as `SharedInstanceClient` and **not spawn** local TCP hubs (Connection then shows misleading “TCP hub unreachable”). 2. **Dead TCP hub still enabled** — outbound queue fills; path requests fail with _no available capacity_. -3. **No propagation node** — when direct link fails, there is no store-and-forward fallback (see next section). +3. **No remote propagation node** — when Direct fails and no preferred **remote** PN is configured, there is no store-and-forward retry (local inbox does not count). With a remote preferred PN, the sidecar retries once via that PN (see **Stale path + Failed via TCP** below). **Fix**: @@ -1160,7 +1160,9 @@ Export for GitHub (`reticulum.sidecar.interfaceIssueAlert`, link-timeout counts) 1. Open **Network → Propagation** (Chat notice **Set up propagation** jumps there). 2. Add a **32-character LXMF destination hash** from whoever runs the propagation node you trust. 3. Set **Preferred** (manual mode) or leave **Auto** when multiple nodes are listed. -4. **Local propagation only** queues messages on this device — it does **not** replace a remote propagation node for peers you cannot reach directly. +4. **Local propagation only** is this device’s offline inbox — it does **not** replace a remote propagation node for peers you cannot reach directly. Preferring Local shows a warning toast; Chat still treats local-only as “no remote PN.” + +**Stale path + Failed via TCP:** When a path exists, mesh-client tries **Direct** first. If Direct fails and a preferred **remote** PN is configured, the sidecar retries once via that PN (Ratspeak-style store-and-forward). Without a remote preferred PN, the row stays **Failed** even if Ratspeak on the same machine deposits successfully. **Not the same as transport:** Ratspeak TCP hubs (e.g. `rns.ratspeak.org:4242`) and [rathole](https://github.com/ratspeak/rathole) are **connectivity / transport** tools, not LXMF propagation. mesh-client does not ship a default community propagation hash. diff --git a/reticulum-sidecar/src/stack/lxmf_outbound.rs b/reticulum-sidecar/src/stack/lxmf_outbound.rs index 155f0db22..4f0248ba1 100644 --- a/reticulum-sidecar/src/stack/lxmf_outbound.rs +++ b/reticulum-sidecar/src/stack/lxmf_outbound.rs @@ -83,6 +83,9 @@ impl PathRequestGate { } } +/// Bound on Direct→PN fallback hash tracking (one entry per outbound message). +const PN_FALLBACK_ATTEMPTED_MAX: usize = 256; + pub struct LxmfOutboundDriver { transport_tx: mpsc::Sender, link_delivery: LinkDeliveryManager, @@ -93,6 +96,8 @@ pub struct LxmfOutboundDriver { pinned_identities: HashMap, path_table_hashes: HashSet, path_request_gate: PathRequestGate, + /// Message hashes that already consumed the one-shot Direct→PN fallback. + pn_fallback_attempted: HashSet<[u8; 32]>, self_lxmf_hash: String, self_display_name: String, } @@ -117,6 +122,7 @@ impl LxmfOutboundDriver { pinned_identities: HashMap::new(), path_table_hashes: HashSet::new(), path_request_gate: PathRequestGate::new(), + pn_fallback_attempted: HashSet::new(), self_lxmf_hash: self_lxmf_hash.clone(), self_display_name, }; @@ -444,6 +450,18 @@ impl LxmfOutboundDriver { } fn fail_outbound_message( + &mut self, + router: &mut LxmRouter, + event_tx: &broadcast::Sender, + message: LxMessage, + ) { + match self.try_requeue_via_propagation(router, event_tx, message) { + Ok(()) => {} + Err(message) => self.emit_outbound_failed(router, event_tx, *message), + } + } + + fn emit_outbound_failed( &mut self, router: &mut LxmRouter, event_tx: &broadcast::Sender, @@ -451,8 +469,14 @@ impl LxmfOutboundDriver { ) { message.mark_failed(); if let Some(hash) = message.hash.or(message.message_id) { + self.pn_fallback_attempted.remove(&hash); let _ = router.mark_outbound_failed(&hash); - emit_outbound_status_by_hash(event_tx, &hash, "failed"); + emit_outbound_status_by_hash( + event_tx, + &hash, + "failed", + Some(delivery_method_label(message.method)), + ); } let method = delivery_method_label(message.method); let payload = lxmf_payload_from_message( @@ -467,6 +491,63 @@ impl LxmfOutboundDriver { emit_outbound_status(event_tx, &payload, "failed", method); } + /// After Direct link failure, deposit once via preferred remote PN (Ratspeak parity). + /// Returns `Ok(())` when re-queued as Propagated; `Err(message)` when caller should fail. + fn try_requeue_via_propagation( + &mut self, + router: &mut LxmRouter, + event_tx: &broadcast::Sender, + mut message: LxMessage, + ) -> Result<(), Box> { + if !should_fallback_direct_to_pn( + message.method, + router.outbound_propagation_node, + &self.self_lxmf_hash, + message + .hash + .or(message.message_id) + .is_some_and(|h| self.pn_fallback_attempted.contains(&h)), + ) { + return Err(Box::new(message)); + } + let Some(msg_hash) = message.hash.or(message.message_id) else { + return Err(Box::new(message)); + }; + // Quietly drop any leftover Direct queue entry (already removed for most Fail paths). + router + .pending_outbound + .retain(|m| m.hash != Some(msg_hash) && m.message_id != Some(msg_hash)); + self.remember_pn_fallback(msg_hash); + message.method = DeliveryMethod::Propagated; + message.delivery_attempts = 0; + message.next_delivery_attempt = 0.0; + tracing::info!( + dest = %hex::encode(message.destination_hash), + msg = %hex::encode(msg_hash), + "LXMF Direct failed; falling back to preferred remote propagation node" + ); + router.send(message); + emit_outbound_status_with_via( + event_tx, + Some(serde_json::Value::String(hex::encode(msg_hash))), + None, + "sending", + Some("propagated"), + None, + ); + Ok(()) + } + + fn remember_pn_fallback(&mut self, msg_hash: [u8; 32]) { + if self.pn_fallback_attempted.len() >= PN_FALLBACK_ATTEMPTED_MAX { + // Evict an arbitrary entry so floods cannot grow unbounded. + if let Some(oldest) = self.pn_fallback_attempted.iter().next().copied() { + self.pn_fallback_attempted.remove(&oldest); + } + } + self.pn_fallback_attempted.insert(msg_hash); + } + fn pack_for_propagation( &self, message: &mut LxMessage, @@ -506,31 +587,21 @@ impl LxmfOutboundDriver { match result { DeliveryResult::Complete { msg_hash, .. } => { if let Some(hash) = msg_hash { + let method = if self.pn_fallback_attempted.contains(&hash) { + Some("propagated") + } else { + None + }; + self.pn_fallback_attempted.remove(&hash); let _ = router.mark_outbound_delivered(&hash); - emit_outbound_status_by_hash(event_tx, &hash, "delivered"); + emit_outbound_status_by_hash(event_tx, &hash, "delivered", method); } } - DeliveryResult::Rejected { - msg_hash, message, .. - } - | DeliveryResult::Failed { - msg_hash, message, .. - } => { - if let Some(hash) = msg_hash { - let _ = router.mark_outbound_failed(&hash); - emit_outbound_status_by_hash(event_tx, &hash, "failed"); + DeliveryResult::Rejected { message, .. } | DeliveryResult::Failed { message, .. } => { + match self.try_requeue_via_propagation(router, event_tx, message) { + Ok(()) => {} + Err(message) => self.emit_outbound_failed(router, event_tx, *message), } - let method = delivery_method_label(message.method); - let payload = lxmf_payload_from_message( - &message, - &self.self_lxmf_hash, - &self.self_display_name, - None, - Some(method), - "outbound", - None, - ); - emit_outbound_status(event_tx, &payload, "failed", method); } } } @@ -570,6 +641,27 @@ pub(crate) fn choose_lxmf_send_route( } } +/// Whether a failed Direct attempt may be re-queued once via preferred remote PN. +pub(crate) fn should_fallback_direct_to_pn( + method: DeliveryMethod, + preferred_pn: Option<[u8; 16]>, + self_lxmf_hash_hex: &str, + already_fallback: bool, +) -> bool { + if already_fallback || method != DeliveryMethod::Direct { + return false; + } + let Some(pn) = preferred_pn else { + return false; + }; + let pn_hex = hex::encode(pn); + // Local / self PN is an offline inbox — not a network store for unreachable peers. + if pn_hex.eq_ignore_ascii_case(self_lxmf_hash_hex.trim()) { + return false; + } + true +} + /// Cap on retained destination public keys (announce / path flood bound). const MAX_KNOWN_IDENTITIES: usize = 4096; @@ -628,13 +720,14 @@ fn emit_outbound_status_by_hash( event_tx: &broadcast::Sender, hash: &[u8; 32], status: &str, + delivery_method: Option<&str>, ) { emit_outbound_status_with_via( event_tx, Some(serde_json::Value::String(hex::encode(hash))), None, status, - None, + delivery_method, None, ); } @@ -843,4 +936,49 @@ mod tests { LxmfSendRoute::Direct ); } + + #[test] + fn should_fallback_direct_to_pn_when_remote_preferred() { + let remote = [0x47u8; 16]; + assert!(should_fallback_direct_to_pn( + DeliveryMethod::Direct, + Some(remote), + &"aa".repeat(16), + false, + )); + } + + #[test] + fn should_fallback_direct_to_pn_rejects_local_self_pn() { + let self_hash = [0x09u8; 16]; + assert!(!should_fallback_direct_to_pn( + DeliveryMethod::Direct, + Some(self_hash), + &hex::encode(self_hash), + false, + )); + } + + #[test] + fn should_fallback_direct_to_pn_rejects_propagated_and_repeat() { + let remote = [0x47u8; 16]; + assert!(!should_fallback_direct_to_pn( + DeliveryMethod::Propagated, + Some(remote), + &"aa".repeat(16), + false, + )); + assert!(!should_fallback_direct_to_pn( + DeliveryMethod::Direct, + Some(remote), + &"aa".repeat(16), + true, + )); + assert!(!should_fallback_direct_to_pn( + DeliveryMethod::Direct, + None, + &"aa".repeat(16), + false, + )); + } } diff --git a/src/main/db-schema-sync.ts b/src/main/db-schema-sync.ts index de76555da..8e3e6334e 100644 --- a/src/main/db-schema-sync.ts +++ b/src/main/db-schema-sync.ts @@ -19,7 +19,7 @@ import { sanitizeLogMessage } from './log-service'; import { ensureMessageFtsTables } from './messageFts'; /** Bumped when ensureSchema behavior changes in a non-idempotent way (rare). */ -export const CURRENT_SCHEMA_VERSION = 46; +export const CURRENT_SCHEMA_VERSION = 47; /** Thrown when on-disk `user_version` exceeds this build's {@link CURRENT_SCHEMA_VERSION}. */ export class DatabaseSchemaTooNewError extends Error { @@ -212,7 +212,8 @@ export const CANONICAL_TABLES_DDL = ` to_hash TEXT, reply_to_hash TEXT, message_hash TEXT, - received_via TEXT + received_via TEXT, + delivery_method TEXT ); CREATE TABLE IF NOT EXISTS rrc_messages ( @@ -439,6 +440,7 @@ export const DESIRED_COLUMNS: Readonly void>; close: ReturnType; + removeAllListeners: ReturnType; options: unknown; } vi.mock('ws', () => ({ default: class MockWebSocket { handlers = new Map void>(); - close = vi.fn(); + // Mirror ws: closing while CONNECTING abortHandshake emits 'error' on nextTick. + close = vi.fn(() => { + process.nextTick(() => { + const err = new Error('WebSocket was closed before the connection was established'); + const handler = this.handlers.get('error'); + if (handler) { + handler(err); + return; + } + // EventEmitter: 'error' with no listeners becomes uncaughtException + process.emit('uncaughtException', err); + }); + }); + removeAllListeners = vi.fn(() => { + this.handlers.clear(); + return this; + }); constructor( public url: string, public options?: unknown, @@ -450,6 +467,42 @@ describe('ReticulumSidecarManager', () => { mkdirSpy.mockRestore(); }); + it('stop while WS is CONNECTING does not raise uncaughtException', async () => { + const existsSpy = vi.spyOn(fs, 'existsSync').mockReturnValue(true); + const mkdirSpy = vi.spyOn(fs, 'mkdirSync').mockImplementation(() => undefined); + const proc = mockSidecarProc(); + proc.kill.mockImplementation(() => { + proc.emit('exit', 0, null); + }); + spawnMock.mockReturnValue(proc); + + const uncaught = vi.fn(); + process.on('uncaughtException', uncaught); + + const manager = new ReticulumSidecarManager(); + try { + await manager.start(); + expect(mockWsInstances.length).toBeGreaterThan(0); + // Do not fire 'open' — leave the socket in CONNECTING so close() abortHandshake-emits. + } finally { + await manager.stop(); + process.off('uncaughtException', uncaught); + existsSpy.mockRestore(); + mkdirSpy.mockRestore(); + } + + await new Promise((resolve) => { + process.nextTick(resolve); + }); + + expect(uncaught).not.toHaveBeenCalled(); + const wsInstance = mockWsInstances[mockWsInstances.length - 1]; + expect(wsInstance.removeAllListeners).toHaveBeenCalled(); + expect(wsInstance.close).toHaveBeenCalled(); + // Teardown re-attaches an error listener after removeAllListeners (before close). + expect(wsInstance.handlers.has('error')).toBe(true); + }); + it('yields Noble BLE when config has enabled ble RNode before spawn', async () => { const existsSpy = vi.spyOn(fs, 'existsSync').mockReturnValue(true); const mkdirSpy = vi.spyOn(fs, 'mkdirSync').mockImplementation(() => undefined); diff --git a/src/main/reticulum-sidecar-manager.ts b/src/main/reticulum-sidecar-manager.ts index f5994770a..9277e3e5e 100644 --- a/src/main/reticulum-sidecar-manager.ts +++ b/src/main/reticulum-sidecar-manager.ts @@ -633,6 +633,10 @@ export class ReticulumSidecarManager extends EventEmitter { close: () => { try { socket.removeAllListeners(); + // ws abortHandshake emits 'error' on nextTick when closed while CONNECTING + socket.on('error', () => { + // catch-no-log-ok: intentional teardown; CONNECTING abort is expected + }); socket.close(); } catch { // catch-no-log-ok: socket may already be closed diff --git a/src/renderer/components/NodeInfoBody.test.tsx b/src/renderer/components/NodeInfoBody.test.tsx index f77f9cf09..7b7af0744 100644 --- a/src/renderer/components/NodeInfoBody.test.tsx +++ b/src/renderer/components/NodeInfoBody.test.tsx @@ -61,6 +61,33 @@ describe('NodeInfoBody', () => { expect(await axe(container)).toHaveNoViolations(); }); + it('has no axe violations when RF diagnostics include an error finding', async () => { + const self = makeNode({ + node_id: 7, + long_name: 'Alpha', + channel_utilization: 0.1, + meshcore_local_stats: { + batteryMilliVolts: 0, + uptimeSecs: 0, + queueLen: 251, + noiseFloor: -110, + lastRssi: 0, + lastSnr: 0, + txAirSecs: 0, + rxAirSecs: 0, + recv: 0, + sent: 0, + nSentFlood: 0, + nSentDirect: 0, + nRecvFlood: 0, + nRecvDirect: 0, + }, + }); + const { container } = render(); + hydrateAxeThemeColors(container); + expect(await axe(container)).toHaveNoViolations(); + }); + it('shows last tracked position when live position is missing', () => { positionHistoryStoreState.history = new Map([ [ diff --git a/src/renderer/components/NodeInfoBody.tsx b/src/renderer/components/NodeInfoBody.tsx index 789566e45..2fbb3d0d3 100644 --- a/src/renderer/components/NodeInfoBody.tsx +++ b/src/renderer/components/NodeInfoBody.tsx @@ -181,11 +181,13 @@ export interface NodeInfoBodyProps { } const SEVERITY_STYLES: Record = { + error: 'text-red-400', warning: 'text-orange-400', info: 'text-blue-400', }; const SEVERITY_ICON: Record = { + error: '✕', warning: '⚠', info: 'ℹ', }; diff --git a/src/renderer/components/ReticulumMessageStatusBadge.test.tsx b/src/renderer/components/ReticulumMessageStatusBadge.test.tsx new file mode 100644 index 000000000..36ca6f3f8 --- /dev/null +++ b/src/renderer/components/ReticulumMessageStatusBadge.test.tsx @@ -0,0 +1,48 @@ +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 { ReticulumMessageStatusBadge } from '@/renderer/components/ReticulumMessageStatusBadge'; +import { hydrateAxeThemeColors } from '@/renderer/lib/a11yTestHelpers'; + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +async function renderAndAssertAxe(ui: ReactElement): Promise> { + const view = render(ui); + hydrateAxeThemeColors(view.container); + expect(await axe(view.container)).toHaveNoViolations(); + return view; +} + +describe('ReticulumMessageStatusBadge', () => { + it('shows Delivered for direct Completes', async () => { + await renderAndAssertAxe( + , + ); + expect( + screen.getByLabelText('chatPanel.sentViaTcp: chatPanel.reticulumSendDelivered'), + ).toBeTruthy(); + }); + + it('shows Stored at PN for propagated Completes', async () => { + await renderAndAssertAxe( + , + ); + expect( + screen.getByLabelText('chatPanel.sentViaPropagation: chatPanel.reticulumSendStoredAtPn'), + ).toBeTruthy(); + expect(screen.getByText(/reticulumPnAbbrev/)).toBeTruthy(); + }); + + it('shows Queued at PN while propagated Sending', () => { + render(); + expect( + screen.getByLabelText('chatPanel.sentViaPropagation: chatPanel.reticulumSendPropagated'), + ).toBeTruthy(); + }); +}); diff --git a/src/renderer/components/ReticulumMessageStatusBadge.tsx b/src/renderer/components/ReticulumMessageStatusBadge.tsx index df92c1cd8..27ec9f3ff 100644 --- a/src/renderer/components/ReticulumMessageStatusBadge.tsx +++ b/src/renderer/components/ReticulumMessageStatusBadge.tsx @@ -1,3 +1,4 @@ +import type { TFunction } from 'i18next'; import { useTranslation } from 'react-i18next'; import { DeliveryStatusBadgeFrame } from '@/renderer/components/DeliveryStatusBadgeFrame'; @@ -15,6 +16,8 @@ export interface ReticulumMessageStatusBadgeProps { error?: string; } +type OutboundStatus = ReticulumMessageStatusBadgeProps['status']; + function tooltipKeyForVia(via: ReticulumVia | undefined): string { switch (via) { case 'rf': @@ -30,6 +33,65 @@ function tooltipKeyForVia(via: ReticulumVia | undefined): string { } } +function statusIcon(status: OutboundStatus): string { + switch (status) { + case 'sending': + return '\u23F3'; + case 'acked': + return '\u2713'; + default: + return '\u2717'; + } +} + +function statusColorClass(status: OutboundStatus): string { + switch (status) { + case 'sending': + return 'text-muted'; + case 'acked': + return 'text-bright-green'; + default: + return 'text-red-400'; + } +} + +function statusLabelText( + t: TFunction, + status: OutboundStatus, + deliveryMethod: MessageRecord['reticulumDeliveryMethod'] | undefined, + error: string | undefined, +): string { + switch (status) { + case 'sending': + if (deliveryMethod === 'propagated') { + return t('chatPanel.reticulumSendPropagated'); + } + return t('chatPanel.reticulumSendSending'); + case 'acked': + if (deliveryMethod === 'propagated') { + return t('chatPanel.reticulumSendStoredAtPn'); + } + return t('chatPanel.reticulumSendDelivered'); + default: + return error ?? t('chatPanel.reticulumSendFailed'); + } +} + +function viaPrefixText( + t: TFunction, + deliveryMethod: MessageRecord['reticulumDeliveryMethod'] | undefined, + atoms: ReticulumVia[], + viasLabel: string, +): string { + if (deliveryMethod === 'propagated') { + return t('chatPanel.sentViaPropagation'); + } + if (atoms.length > 1) { + return t('chatPanel.sentViaMultiple', { vias: viasLabel }); + } + return t(tooltipKeyForVia(atoms[0])); +} + export function ReticulumMessageStatusBadge({ status, via, @@ -37,28 +99,18 @@ export function ReticulumMessageStatusBadge({ error, }: ReticulumMessageStatusBadgeProps) { const { t } = useTranslation(); - const icon = status === 'sending' ? '\u23F3' : status === 'acked' ? '\u2713' : '\u2717'; - const colorClass = - status === 'sending' ? 'text-muted' : status === 'acked' ? 'text-bright-green' : 'text-red-400'; const atoms = parseReticulumViaAtoms(via); const viasLabel = formatReticulumViaBadgeLabel(via ?? 'network'); - const label = deliveryMethod === 'propagated' ? 'PN' : viasLabel; - const statusLabel = - status === 'sending' - ? deliveryMethod === 'propagated' - ? t('chatPanel.reticulumSendPropagated') - : t('chatPanel.reticulumSendSending') - : status === 'acked' - ? t('chatPanel.reticulumSendDelivered') - : (error ?? t('chatPanel.reticulumSendFailed')); - const viaPrefix = - deliveryMethod === 'propagated' - ? t('chatPanel.sentViaPropagation') - : atoms.length > 1 - ? t('chatPanel.sentViaMultiple', { vias: viasLabel }) - : t(tooltipKeyForVia(atoms[0])); + const label = deliveryMethod === 'propagated' ? t('chatPanel.reticulumPnAbbrev') : viasLabel; + const statusLabel = statusLabelText(t, status, deliveryMethod, error); + const viaPrefix = viaPrefixText(t, deliveryMethod, atoms, viasLabel); const tooltip = `${viaPrefix}: ${statusLabel}`; return ( - + ); } diff --git a/src/renderer/components/ReticulumPropagationSection.test.tsx b/src/renderer/components/ReticulumPropagationSection.test.tsx index 01092358d..22da0d04d 100644 --- a/src/renderer/components/ReticulumPropagationSection.test.tsx +++ b/src/renderer/components/ReticulumPropagationSection.test.tsx @@ -101,6 +101,8 @@ describe('ReticulumPropagationSection', () => { it('shows rename and delete for remote nodes only', () => { render(); + expect(screen.getByText(/reticulumPropagation\.localInboxName/)).toBeInTheDocument(); + expect(screen.getByText('reticulumPropagation.localInboxHint')).toBeInTheDocument(); expect( screen.getByRole('button', { name: 'reticulumPropagation.renameAria:Remote hub' }), ).toBeInTheDocument(); @@ -119,6 +121,31 @@ describe('ReticulumPropagationSection', () => { ).not.toBeInTheDocument(); }); + it('warns when preferring local-only propagation', async () => { + const user = userEvent.setup(); + const setPreferredOnSidecar = vi.mocked( + useReticulumPropagationStore.getState().setPreferredOnSidecar, + ); + render(); + const preferButtons = screen.getAllByRole('button', { + name: 'reticulumPropagation.setPreferred', + }); + const localPrefer = preferButtons.at(0); + if (!localPrefer) { + throw new Error('expected Set preferred control for local-prop'); + } + await user.click(localPrefer); + await waitFor(() => { + expect(setPreferredOnSidecar).toHaveBeenCalledWith('local-prop'); + }); + await waitFor(() => { + expect(addToast).toHaveBeenCalledWith( + 'reticulumPropagation.preferredLocalWarning', + 'warning', + ); + }); + }); + it('confirms delete and calls removePropagationNode', async () => { const user = userEvent.setup(); const removePropagationNode = vi.mocked( @@ -203,4 +230,21 @@ describe('ReticulumPropagationSection', () => { }); expect(screen.getByLabelText('reticulumPropagation.renameLabel')).toBeInTheDocument(); }); + + it('toasts when set preferred fails', async () => { + const user = userEvent.setup(); + useReticulumPropagationStore.setState({ + setPreferredOnSidecar: vi.fn().mockResolvedValue(false), + }); + + render(); + + await user.click( + screen.getAllByRole('button', { name: 'reticulumPropagation.setPreferred' })[0], + ); + + await waitFor(() => { + expect(addToast).toHaveBeenCalledWith('reticulumPropagation.setPreferredFailed', 'error'); + }); + }); }); diff --git a/src/renderer/components/ReticulumPropagationSection.tsx b/src/renderer/components/ReticulumPropagationSection.tsx index 3281ed379..0a6d43a46 100644 --- a/src/renderer/components/ReticulumPropagationSection.tsx +++ b/src/renderer/components/ReticulumPropagationSection.tsx @@ -275,7 +275,8 @@ export default function ReticulumPropagationSection({ ) : ( <> - {node.name} ({formatPropagationNodeStatus(node.status, t)}) + {isLocal ? t('reticulumPropagation.localInboxName') : node.name} ( + {formatPropagationNodeStatus(node.status, t)}) {isLocal && node.message_count != null ? ( {t('reticulumPropagation.localInboxStats', { @@ -297,7 +298,17 @@ export default function ReticulumPropagationSection({ type="button" className="text-xs text-amber-400 hover:underline" onClick={() => { - void setPreferredOnSidecar(node.id); + void setPreferredOnSidecar(node.id) + .then((ok) => { + if (ok && isLocal) { + addToast(t('reticulumPropagation.preferredLocalWarning'), 'warning'); + } else if (!ok) { + addToast(t('reticulumPropagation.setPreferredFailed'), 'error'); + } + }) + .catch(() => { + addToast(t('reticulumPropagation.setPreferredFailed'), 'error'); + }); }} aria-label={t('reticulumPropagation.setPreferred')} > @@ -407,6 +418,7 @@ export default function ReticulumPropagationSection({ {t('reticulumPropagation.syncNow')} +

{t('reticulumPropagation.localInboxHint')}

{t('reticulumPropagation.syncPathHint')}

{lastSyncError === 'reticulumPropagation.syncEstablishNoLinkProof' ? ( diff --git a/src/renderer/lib/diagnostics/RFDiagnosticEngine.test.ts b/src/renderer/lib/diagnostics/RFDiagnosticEngine.test.ts index bbb7f0057..80ac8bd91 100644 --- a/src/renderer/lib/diagnostics/RFDiagnosticEngine.test.ts +++ b/src/renderer/lib/diagnostics/RFDiagnosticEngine.test.ts @@ -241,4 +241,77 @@ describe('diagnoseConnectedNode — MeshCore stats', () => { const findings = diagnoseConnectedNode(node); expect(findings.some((f) => f.condition === 'Excessive Flooding')).toBe(false); }); + + it('flags High Companion TX Queue as warning when queueLen > 200', () => { + const node = baseNode({ + meshcore_local_stats: { + batteryMilliVolts: 0, + uptimeSecs: 0, + queueLen: 201, + noiseFloor: -110, + lastRssi: 0, + lastSnr: 0, + txAirSecs: 0, + rxAirSecs: 0, + recv: 0, + sent: 0, + nSentFlood: 0, + nSentDirect: 0, + nRecvFlood: 0, + nRecvDirect: 0, + }, + }); + const findings = diagnoseConnectedNode(node); + const row = findings.find((f) => f.condition === 'High Companion TX Queue'); + expect(row).toBeDefined(); + expect(row?.severity).toBe('warning'); + }); + + it('flags High Companion TX Queue as error when queueLen > 250', () => { + const node = baseNode({ + meshcore_local_stats: { + batteryMilliVolts: 0, + uptimeSecs: 0, + queueLen: 251, + noiseFloor: -110, + lastRssi: 0, + lastSnr: 0, + txAirSecs: 0, + rxAirSecs: 0, + recv: 0, + sent: 0, + nSentFlood: 0, + nSentDirect: 0, + nRecvFlood: 0, + nRecvDirect: 0, + }, + }); + const findings = diagnoseConnectedNode(node); + const row = findings.find((f) => f.condition === 'High Companion TX Queue'); + expect(row).toBeDefined(); + expect(row?.severity).toBe('error'); + }); + + it('does not flag High Companion TX Queue when queueLen <= 200', () => { + const node = baseNode({ + meshcore_local_stats: { + batteryMilliVolts: 0, + uptimeSecs: 0, + queueLen: 200, + noiseFloor: -110, + lastRssi: 0, + lastSnr: 0, + txAirSecs: 0, + rxAirSecs: 0, + recv: 0, + sent: 0, + nSentFlood: 0, + nSentDirect: 0, + nRecvFlood: 0, + nRecvDirect: 0, + }, + }); + const findings = diagnoseConnectedNode(node); + expect(findings.some((f) => f.condition === 'High Companion TX Queue')).toBe(false); + }); }); diff --git a/src/renderer/lib/diagnostics/RFDiagnosticEngine.ts b/src/renderer/lib/diagnostics/RFDiagnosticEngine.ts index da8857da2..95c282484 100644 --- a/src/renderer/lib/diagnostics/RFDiagnosticEngine.ts +++ b/src/renderer/lib/diagnostics/RFDiagnosticEngine.ts @@ -7,7 +7,7 @@ import { snrMeaningfulForNodeDiagnostics } from './snrMeaningfulForNodeDiagnosti export interface RFDiagnosis { condition: string; cause: string; - severity: 'warning' | 'info'; + severity: 'error' | 'warning' | 'info'; /** When true, SNR-based interpretation is last-hop only (remote nodes). */ isLastHop?: boolean; /** Extra lines under cause (template-only; render muted in UI). */ @@ -213,6 +213,19 @@ export function diagnoseConnectedNode( }, }); } + + const queueLen = meshcoreStats.queueLen; + if (typeof queueLen === 'number' && queueLen > 200) { + findings.push({ + condition: 'High Companion TX Queue', + cause: `Companion outbound queue depth is ${queueLen}/256 — traffic may be delayed or dropped.`, + severity: queueLen > 250 ? 'error' : 'warning', + causeI18n: { + key: 'diagnosticsPanel.rfCause.highCompanionTxQueue', + params: { queueLen }, + }, + }); + } } return findings; diff --git a/src/renderer/lib/diagnostics/RemediationEngine.test.ts b/src/renderer/lib/diagnostics/RemediationEngine.test.ts index 1c0af38e6..c297263c0 100644 --- a/src/renderer/lib/diagnostics/RemediationEngine.test.ts +++ b/src/renderer/lib/diagnostics/RemediationEngine.test.ts @@ -34,6 +34,7 @@ describe('getRecommendedActionForRfCondition', () => { 'Fringe / Weak Coverage', 'Elevated Noise Floor', 'Excessive Flooding', + 'High Companion TX Queue', ]; for (const condition of known) { const result = getRecommendedActionForRfCondition(condition); diff --git a/src/renderer/lib/diagnostics/RemediationEngine.ts b/src/renderer/lib/diagnostics/RemediationEngine.ts index a93057c96..70bc76bd6 100644 --- a/src/renderer/lib/diagnostics/RemediationEngine.ts +++ b/src/renderer/lib/diagnostics/RemediationEngine.ts @@ -229,6 +229,15 @@ const RF_CONDITION_REMEDIES: Record = { titleKey: 'diagnosticsPanel.remedyRf.excessiveFlooding.title', descriptionKey: 'diagnosticsPanel.remedyRf.excessiveFlooding.description', }, + 'High Companion TX Queue': { + title: 'Reduce outbound traffic or check the RF link', + description: + 'Companion TX queue is nearly full — pause sends, reduce floods, or check for interference and weak links.', + category: 'Physical', + severity: 'critical', + titleKey: 'diagnosticsPanel.remedyRf.highCompanionTxQueue.title', + descriptionKey: 'diagnosticsPanel.remedyRf.highCompanionTxQueue.description', + }, }; export function getRecommendedActionForRfCondition(condition: string): DiagnosticRemedy | null { diff --git a/src/renderer/lib/diagnostics/ReticulumDiagnosticEngine.test.ts b/src/renderer/lib/diagnostics/ReticulumDiagnosticEngine.test.ts index bdc9238e5..959df55b6 100644 --- a/src/renderer/lib/diagnostics/ReticulumDiagnosticEngine.test.ts +++ b/src/renderer/lib/diagnostics/ReticulumDiagnosticEngine.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import type { RfDiagnosticRow } from '@/renderer/lib/types'; @@ -331,6 +331,186 @@ describe('ReticulumDiagnosticEngine', () => { ).toBe(true); }); + it('flags sidecar-unhealthy when running and unhealthy past grace', () => { + const rows = buildReticulumDiagnosticRows( + { rns_ready: true, lxmf_ready: true, interface_count: 1, peer_count: 1 }, + { + sidecarRunning: true, + sidecarHealthy: false, + sidecarUnhealthySince: Date.now() - 65_000, + }, + ); + const row = rows.find( + (r): r is RfDiagnosticRow => r.kind === 'rf' && r.condition === 'reticulum/sidecar-unhealthy', + ); + expect(row).toBeDefined(); + expect(row?.severity).toBe('error'); + expect(row?.causeI18n?.key).toBe('diagnosticsPanel.reticulum.runtime.sidecarUnhealthy'); + expect(row?.reticulumRepairKind).toBe('restart_stack'); + }); + + it('does not flag sidecar-unhealthy within grace or when since is missing', () => { + expect( + buildReticulumDiagnosticRows( + { rns_ready: true, lxmf_ready: true, interface_count: 1, peer_count: 1 }, + { + sidecarRunning: true, + sidecarHealthy: false, + sidecarUnhealthySince: Date.now() - 5_000, + }, + ).some((r) => r.kind === 'rf' && r.condition === 'reticulum/sidecar-unhealthy'), + ).toBe(false); + expect( + buildReticulumDiagnosticRows( + { rns_ready: true, lxmf_ready: true, interface_count: 1, peer_count: 1 }, + { sidecarRunning: true, sidecarHealthy: false }, + ).some((r) => r.kind === 'rf' && r.condition === 'reticulum/sidecar-unhealthy'), + ).toBe(false); + }); + + it('does not flag sidecar-unhealthy when healthy or not running', () => { + expect( + buildReticulumDiagnosticRows( + { rns_ready: true, lxmf_ready: true, interface_count: 1, peer_count: 1 }, + { sidecarRunning: true, sidecarHealthy: true }, + ).some((r) => r.kind === 'rf' && r.condition === 'reticulum/sidecar-unhealthy'), + ).toBe(false); + expect( + buildReticulumDiagnosticRows( + { rns_ready: true, lxmf_ready: true, interface_count: 1, peer_count: 1 }, + { sidecarRunning: false, sidecarHealthy: false }, + ).some((r) => r.kind === 'rf' && r.condition === 'reticulum/sidecar-unhealthy'), + ).toBe(false); + }); + + it('flags propagation-sync-stuck when establishing past stall window', () => { + const rows = buildReticulumDiagnosticRows( + { rns_ready: true, lxmf_ready: true, interface_count: 1, peer_count: 1 }, + { + propagation: { + syncActive: true, + syncProgress: 5, + lastSyncError: null, + lastAttemptAt: Date.now() - 50_000, + }, + }, + ); + const row = rows.find( + (r): r is RfDiagnosticRow => + r.kind === 'rf' && r.condition === 'reticulum/propagation-sync-stuck', + ); + expect(row).toBeDefined(); + expect(row?.severity).toBe('warning'); + expect(row?.causeI18n?.key).toBe('diagnosticsPanel.reticulum.runtime.propagationSyncStuck'); + }); + + it('does not flag stuck when progress is past establishing', () => { + const rows = buildReticulumDiagnosticRows( + { rns_ready: true, lxmf_ready: true, interface_count: 1, peer_count: 1 }, + { + propagation: { + syncActive: true, + syncProgress: 40, + lastSyncError: null, + lastAttemptAt: Date.now() - 50_000, + }, + }, + ); + expect( + rows.some((r) => r.kind === 'rf' && r.condition === 'reticulum/propagation-sync-stuck'), + ).toBe(false); + }); + + it('flags propagation-sync-failing when idle with lastSyncError', () => { + const rows = buildReticulumDiagnosticRows( + { rns_ready: true, lxmf_ready: true, interface_count: 1, peer_count: 1 }, + { + propagation: { + syncActive: false, + syncProgress: 0, + lastSyncError: 'reticulumPropagation.syncTimedOut', + lastAttemptAt: Date.now() - 60_000, + }, + }, + ); + const row = rows.find( + (r): r is RfDiagnosticRow => + r.kind === 'rf' && r.condition === 'reticulum/propagation-sync-failing', + ); + expect(row).toBeDefined(); + expect(row?.causeI18n?.key).toBe('diagnosticsPanel.reticulum.runtime.propagationSyncFailing'); + }); + + it('ignores user-cancelled propagation sync as failing', () => { + const rows = buildReticulumDiagnosticRows( + { rns_ready: true, lxmf_ready: true, interface_count: 1, peer_count: 1 }, + { + propagation: { + syncActive: false, + syncProgress: 0, + lastSyncError: 'reticulumPropagation.syncCancelled', + lastAttemptAt: Date.now() - 1_000, + }, + }, + ); + expect( + rows.some((r) => r.kind === 'rf' && r.condition === 'reticulum/propagation-sync-failing'), + ).toBe(false); + }); + + it('does not flag failing when last attempt is older than TTL', () => { + const rows = buildReticulumDiagnosticRows( + { rns_ready: true, lxmf_ready: true, interface_count: 1, peer_count: 1 }, + { + propagation: { + syncActive: false, + syncProgress: 0, + lastSyncError: 'reticulumPropagation.syncTimedOut', + lastAttemptAt: Date.now() - 2 * 60 * 60 * 1000, + }, + }, + ); + expect( + rows.some((r) => r.kind === 'rf' && r.condition === 'reticulum/propagation-sync-failing'), + ).toBe(false); + }); + + it('does not flag stuck at exact sub-stall boundary or without attemptAt', () => { + vi.useFakeTimers(); + try { + const now = 1_700_000_000_000; + vi.setSystemTime(now); + expect( + buildReticulumDiagnosticRows( + { rns_ready: true, lxmf_ready: true, interface_count: 1, peer_count: 1 }, + { + propagation: { + syncActive: true, + syncProgress: 5, + lastSyncError: null, + lastAttemptAt: now - 44_999, + }, + }, + ).some((r) => r.kind === 'rf' && r.condition === 'reticulum/propagation-sync-stuck'), + ).toBe(false); + expect( + buildReticulumDiagnosticRows( + { rns_ready: true, lxmf_ready: true, interface_count: 1, peer_count: 1 }, + { + propagation: { + syncActive: true, + syncProgress: 5, + lastSyncError: null, + lastAttemptAt: null, + }, + }, + ).some((r) => r.kind === 'rf' && r.condition === 'reticulum/propagation-sync-stuck'), + ).toBe(false); + } finally { + vi.useRealTimers(); + } + }); + it('mergeReticulumDiagnosticRows replaces prior reticulum rows', () => { const merged = mergeReticulumDiagnosticRows( [ diff --git a/src/renderer/lib/diagnostics/ReticulumDiagnosticEngine.ts b/src/renderer/lib/diagnostics/ReticulumDiagnosticEngine.ts index 4ba2e2ae3..8331ad6d1 100644 --- a/src/renderer/lib/diagnostics/ReticulumDiagnosticEngine.ts +++ b/src/renderer/lib/diagnostics/ReticulumDiagnosticEngine.ts @@ -10,7 +10,12 @@ import { isReticulumRemoteInterfaceType, type ReticulumLocalInterfaceInput, } from '@/renderer/lib/reticulum/reticulumLocalInterfaceHealth'; +import { + isPropagationSyncEstablishingStuck, + RETICULUM_PROPAGATION_SYNC_FAILING_DIAGNOSTIC_TTL_MS, +} from '@/renderer/lib/reticulum/reticulumPropagationSync'; import { type DiagnosticRow, rfRowId } from '@/renderer/lib/types'; +import { PROPAGATION_SYNC_USER_CANCEL_KEY } from '@/renderer/stores/reticulumPropagationStore'; import type { ReticulumAutoBeaconAlert, ReticulumInterfaceIssueAlert, @@ -26,6 +31,15 @@ export interface ReticulumDiagnosticsSnapshot { interfaces?: ReticulumLocalInterfaceInput[]; } +/** Propagation sync snapshot for diagnostics (derived from reticulumPropagationStore). */ +export interface ReticulumPropagationDiagnosticsInput { + syncActive: boolean; + syncProgress: number; + lastSyncError: string | null; + /** Epoch ms when the current/last sync attempt started. */ + lastAttemptAt: number | null; +} + export interface ReticulumDiagnosticsBuildOptions { selfNodeId?: number; interfaces?: ReticulumLocalInterfaceInput[]; @@ -35,6 +49,11 @@ export interface ReticulumDiagnosticsBuildOptions { interfaceIssueAlert?: ReticulumInterfaceIssueAlert | null; /** When true, append shared-instance conflict hint on transport saturation rows. */ shareInstanceEnabled?: boolean; + /** Sidecar hung watchdog — only emit when running && healthy === false. */ + sidecarRunning?: boolean; + sidecarHealthy?: boolean; + sidecarUnhealthySince?: number; + propagation?: ReticulumPropagationDiagnosticsInput; } function runtimeCauseI18n( @@ -63,8 +82,14 @@ export const RETICULUM_RUNTIME_CAUSE_I18N_KEYS = [ 'diagnosticsPanel.reticulum.runtime.transportSaturated', 'diagnosticsPanel.reticulum.runtime.transportSaturatedShareInstance', 'diagnosticsPanel.reticulum.runtime.slowTransportQuery', + 'diagnosticsPanel.reticulum.runtime.sidecarUnhealthy', + 'diagnosticsPanel.reticulum.runtime.propagationSyncStuck', + 'diagnosticsPanel.reticulum.runtime.propagationSyncFailing', ] as const; +/** Sidecar must stay unhealthy this long before emitting an error diagnostic. */ +export const RETICULUM_SIDECAR_UNHEALTHY_DIAGNOSTIC_GRACE_MS = 60_000; + /** Build Reticulum-native diagnostic rows (interface/path/LXMF — not LoRa RF). */ export function buildReticulumDiagnosticRows( snapshot: ReticulumDiagnosticsSnapshot, @@ -359,6 +384,68 @@ export function buildReticulumDiagnosticRows( }); } + if (options?.sidecarRunning === true && options.sidecarHealthy === false) { + const unhealthySince = options.sidecarUnhealthySince; + const pastGrace = + unhealthySince != null && + now - unhealthySince >= RETICULUM_SIDECAR_UNHEALTHY_DIAGNOSTIC_GRACE_MS; + if (pastGrace) { + rows.push({ + kind: 'rf', + id: rfRowId(homeNodeId, 'reticulum/sidecar-unhealthy'), + nodeId: homeNodeId, + condition: 'reticulum/sidecar-unhealthy', + cause: 'Reticulum sidecar is running but not responding to health checks', + causeI18n: runtimeCauseI18n('sidecarUnhealthy'), + severity: 'error', + detectedAt: now, + reticulumRepairKind: 'restart_stack', + }); + } + } + + const propagation = options?.propagation; + if (propagation) { + const attemptAt = propagation.lastAttemptAt; + const stuck = isPropagationSyncEstablishingStuck( + { + syncActive: propagation.syncActive, + syncProgress: propagation.syncProgress, + lastAttemptAt: attemptAt, + }, + now, + ); + if (stuck) { + rows.push({ + kind: 'rf', + id: rfRowId(homeNodeId, 'reticulum/propagation-sync-stuck'), + nodeId: homeNodeId, + condition: 'reticulum/propagation-sync-stuck', + cause: 'Propagation node sync is stuck establishing a link', + causeI18n: runtimeCauseI18n('propagationSyncStuck'), + severity: 'warning', + detectedAt: now, + }); + } else if ( + !propagation.syncActive && + propagation.lastSyncError != null && + propagation.lastSyncError !== PROPAGATION_SYNC_USER_CANCEL_KEY && + attemptAt != null && + now - attemptAt <= RETICULUM_PROPAGATION_SYNC_FAILING_DIAGNOSTIC_TTL_MS + ) { + rows.push({ + kind: 'rf', + id: rfRowId(homeNodeId, 'reticulum/propagation-sync-failing'), + nodeId: homeNodeId, + condition: 'reticulum/propagation-sync-failing', + cause: 'Propagation node sync failed', + causeI18n: runtimeCauseI18n('propagationSyncFailing'), + severity: 'warning', + detectedAt: now, + }); + } + } + return rows; } diff --git a/src/renderer/lib/diagnostics/diagnosticsLabels.ts b/src/renderer/lib/diagnostics/diagnosticsLabels.ts index fd4111ab3..7d6adcda6 100644 --- a/src/renderer/lib/diagnostics/diagnosticsLabels.ts +++ b/src/renderer/lib/diagnostics/diagnosticsLabels.ts @@ -54,6 +54,7 @@ const RF_CONDITION_LABEL_KEY: Record = { 'diagnosticsPanel.rfCondition.potentialMeshcoreRepeaterConflict', 'Elevated Noise Floor': 'diagnosticsPanel.rfCondition.elevatedNoiseFloor', 'Excessive Flooding': 'diagnosticsPanel.rfCondition.excessiveFlooding', + 'High Companion TX Queue': 'diagnosticsPanel.rfCondition.highCompanionTxQueue', }; const ROUTING_ANOMALY_TYPE_KEY: Record = { diff --git a/src/renderer/lib/ingest/reticulumIngest.ts b/src/renderer/lib/ingest/reticulumIngest.ts index 83399a3f5..d96c081fc 100644 --- a/src/renderer/lib/ingest/reticulumIngest.ts +++ b/src/renderer/lib/ingest/reticulumIngest.ts @@ -26,6 +26,7 @@ import { useBlockStore } from '@/renderer/stores/blockStore'; import type { MessageRecord, MessageStatus } from '@/renderer/stores/messageStore'; import { addMessage, upsertMessage, useMessageStore } from '@/renderer/stores/messageStore'; import { useReticulumPeerStore } from '@/renderer/stores/reticulumPeerStore'; +import { parseReticulumDeliveryMethod } from '@/shared/reticulumDeliveryMethod'; import { isReticulumHashPrefixAlias, reticulumRealDisplayName, @@ -63,13 +64,6 @@ export function reticulumContactDisplayNameFromPayload( return reticulumRealDisplayName(p.sender_hash, p.sender_name) ?? undefined; } -function parseReticulumDeliveryMethod( - value: string | undefined, -): MessageRecord['reticulumDeliveryMethod'] { - if (value === 'direct' || value === 'propagated' || value === 'opportunistic') return value; - return undefined; -} - function mapDeliveryStatusToMessageStatus( deliveryStatus: string | null | undefined, direction?: string, @@ -331,6 +325,9 @@ export function persistReticulumOutboundRecord( message_hash: record.reticulumMessageHash ?? record.id, received_via: record.receivedVia ?? null, delivery_status: deliveryStatus, + ...(record.reticulumDeliveryMethod + ? { delivery_method: record.reticulumDeliveryMethod } + : {}), }) .catch((e: unknown) => { console.warn('[reticulumIngest] save outbound ' + errLikeToLogString(e)); diff --git a/src/renderer/lib/reticulum/applyReticulumOutboundDeliveryStatus.test.ts b/src/renderer/lib/reticulum/applyReticulumOutboundDeliveryStatus.test.ts index 1bc2be29a..1939e3453 100644 --- a/src/renderer/lib/reticulum/applyReticulumOutboundDeliveryStatus.test.ts +++ b/src/renderer/lib/reticulum/applyReticulumOutboundDeliveryStatus.test.ts @@ -18,7 +18,7 @@ import { createElectronAPIMock } from '@/renderer/vitest.electronApiMock'; const DEST = '5526a65d0b4d23448206fd3485b76f5b'; const SELF = '8fd7a9361aca12360c7985bc934bdd20'; const identityId = 'reticulum-persist-test'; -const messageHash = 'abc123deliveredhash'; +const messageHash = 'abc123def4567890abc123def4567890abc123def4567890abc123def4567890'; describe('applyReticulumOutboundDeliveryStatus', () => { beforeEach(() => { @@ -27,10 +27,12 @@ describe('applyReticulumOutboundDeliveryStatus', () => { window.electronAPI = createElectronAPIMock(); }); - it('maps delivered/failed/other wire statuses', () => { + it('maps delivered/failed/sending; drops unknown wire statuses', () => { expect(mapLxmfOutboundWireStatus('delivered')).toBe('acked'); expect(mapLxmfOutboundWireStatus('failed')).toBe('failed'); expect(mapLxmfOutboundWireStatus('sending')).toBe('sending'); + expect(mapLxmfOutboundWireStatus('queued')).toBeNull(); + expect(mapLxmfOutboundWireStatus('garbage')).toBeNull(); }); it('marks Completes as acked and persists delivery_status delivered', () => { @@ -215,4 +217,133 @@ describe('applyReticulumOutboundDeliveryStatus', () => { }), ); }); + + it('upgrades reticulumDeliveryMethod on Direct→PN fallback and persists', () => { + const toNodeId = reticulumHashToNodeId(DEST); + const selfNodeId = reticulumHashToNodeId(SELF); + registerReticulumDestinationHash(toNodeId, DEST); + registerReticulumDestinationHash(selfNodeId, SELF); + useMessageStore.setState({ + messages: { + [identityId]: { + [messageHash]: { + id: messageHash, + from: selfNodeId, + to: toNodeId, + senderName: 'Me', + payload: 'fallback', + channelIndex: 0, + timestamp: Date.now(), + status: 'sending', + reticulumMessageHash: messageHash, + reticulumSenderHash: SELF, + reticulumDeliveryMethod: 'direct', + }, + }, + }, + }); + + applyReticulumOutboundDeliveryStatus(identityId, messageHash, 'sending', { + deliveryMethod: 'propagated', + }); + expect( + useMessageStore.getState().messages[identityId]?.[messageHash]?.reticulumDeliveryMethod, + ).toBe('propagated'); + + applyReticulumOutboundDeliveryStatus(identityId, messageHash, 'delivered', { + deliveryMethod: 'propagated', + }); + expect(useMessageStore.getState().messages[identityId]?.[messageHash]?.status).toBe('acked'); + expect( + useMessageStore.getState().messages[identityId]?.[messageHash]?.reticulumDeliveryMethod, + ).toBe('propagated'); + expect(window.electronAPI.db.saveReticulumMessage).toHaveBeenCalledWith( + expect.objectContaining({ + message_hash: messageHash, + delivery_status: 'delivered', + delivery_method: 'propagated', + }), + ); + }); + + it('buffers early deliveryMethod upgrade until flush after rekey', () => { + applyReticulumOutboundDeliveryStatus(identityId, messageHash, 'sending', { + deliveryMethod: 'propagated', + }); + expect(useMessageStore.getState().messages[identityId]?.[messageHash]).toBeUndefined(); + + useMessageStore.setState({ + messages: { + [identityId]: { + [messageHash]: { + id: messageHash, + from: 1, + to: 2, + payload: 'early', + channelIndex: 0, + timestamp: Date.now(), + status: 'sending', + reticulumMessageHash: messageHash, + reticulumSenderHash: SELF, + reticulumDeliveryMethod: 'direct', + }, + }, + }, + }); + + expect(flushPendingReticulumOutboundDeliveryStatus(identityId, messageHash)).toBe(true); + expect( + useMessageStore.getState().messages[identityId]?.[messageHash]?.reticulumDeliveryMethod, + ).toBe('propagated'); + }); + + it('patches deliveryMethod on terminal row without regressing status', () => { + useMessageStore.setState({ + messages: { + [identityId]: { + [messageHash]: { + id: messageHash, + from: 1, + to: 2, + payload: 'done', + channelIndex: 0, + timestamp: Date.now(), + status: 'acked', + reticulumMessageHash: messageHash, + reticulumSenderHash: SELF, + reticulumDeliveryMethod: 'direct', + }, + }, + }, + }); + + applyReticulumOutboundDeliveryStatus(identityId, messageHash, 'sending', { + deliveryMethod: 'propagated', + }); + expect(useMessageStore.getState().messages[identityId]?.[messageHash]?.status).toBe('acked'); + expect( + useMessageStore.getState().messages[identityId]?.[messageHash]?.reticulumDeliveryMethod, + ).toBe('propagated'); + }); + + it('drops invalid message_hash and unknown wire status', () => { + useMessageStore.setState({ + messages: { + [identityId]: { + [messageHash]: { + id: messageHash, + from: 1, + to: 2, + payload: 'x', + channelIndex: 0, + timestamp: Date.now(), + status: 'sending', + }, + }, + }, + }); + applyReticulumOutboundDeliveryStatus(identityId, 'not-a-hash!!!', 'delivered'); + applyReticulumOutboundDeliveryStatus(identityId, messageHash, 'queued'); + expect(useMessageStore.getState().messages[identityId]?.[messageHash]?.status).toBe('sending'); + }); }); diff --git a/src/renderer/lib/reticulum/applyReticulumOutboundDeliveryStatus.ts b/src/renderer/lib/reticulum/applyReticulumOutboundDeliveryStatus.ts index 680c9f4f1..bb50c78aa 100644 --- a/src/renderer/lib/reticulum/applyReticulumOutboundDeliveryStatus.ts +++ b/src/renderer/lib/reticulum/applyReticulumOutboundDeliveryStatus.ts @@ -7,6 +7,10 @@ import { reticulumViaToMessageTransport, } from '@/renderer/lib/reticulum/classifyReticulumVia'; import { resolveReticulumDestinationHash } from '@/renderer/lib/reticulum/destHash'; +import { + isValidReticulumOutboundMessageHash, + normalizeReticulumMessageHash, +} from '@/renderer/lib/reticulum/reticulumMessageHash'; import type { IdentityId } from '@/renderer/lib/types'; import { type MessageRecord, @@ -17,12 +21,14 @@ import { useMessageStore, } from '@/renderer/stores/messageStore'; import { reticulumHashForNodeId } from '@/renderer/stores/reticulumPeerStore'; +import { parseReticulumDeliveryMethod } from '@/shared/reticulumDeliveryMethod'; -/** Map sidecar `lxmf_outbound_status` wire status to UI store status. */ -export function mapLxmfOutboundWireStatus(wireStatus: string): MessageStatus { +/** Map sidecar `lxmf_outbound_status` wire status to UI store status. Unknown → null. */ +export function mapLxmfOutboundWireStatus(wireStatus: string): MessageStatus | null { if (wireStatus === 'delivered') return 'acked'; if (wireStatus === 'failed') return 'failed'; - return 'sending'; + if (wireStatus === 'sending') return 'sending'; + return null; } function resolveOutboundPeerHash(record: MessageRecord): string | null { @@ -53,7 +59,7 @@ const PENDING_DELIVERY_STATUS_TTL_MS = 60_000; const PENDING_DELIVERY_STATUS_MAX = 64; const pendingDeliveryByKey = new Map< string, - { wireStatus: string; sentVia?: string; receivedAt: number } + { wireStatus: string; sentVia?: string; deliveryMethod?: string; receivedAt: number } >(); function pendingDeliveryKey(identityId: IdentityId, messageHash: string): string { @@ -78,11 +84,13 @@ function bufferPendingDeliveryStatus( messageHash: string, wireStatus: string, sentVia?: string, + deliveryMethod?: string, ): void { prunePendingDeliveryStatuses(); pendingDeliveryByKey.set(pendingDeliveryKey(identityId, messageHash), { wireStatus, sentVia, + deliveryMethod, receivedAt: Date.now(), }); } @@ -98,12 +106,18 @@ export function flushPendingReticulumOutboundDeliveryStatus( const key = pendingDeliveryKey(identityId, messageHash); const pending = pendingDeliveryByKey.get(key); if (!pending) return false; + const mapped = mapLxmfOutboundWireStatus(pending.wireStatus); + if (mapped == null) { + pendingDeliveryByKey.delete(key); + return false; + } const applied = persistReticulumOutboundMessageStatus( identityId, messageHash, - mapLxmfOutboundWireStatus(pending.wireStatus), + mapped, undefined, parseWireSentVia(pending.sentVia), + parseReticulumDeliveryMethod(pending.deliveryMethod), ); if (applied) pendingDeliveryByKey.delete(key); return applied; @@ -118,6 +132,7 @@ export function clearPendingReticulumOutboundDeliveryStatusesForTests(): void { * Update Zustand and persist terminal delivery status to SQLite so restart * hydration / stale marking do not flip Completes to failed. * When `sentVia` is set (egress evidence upgrade), also patch store + SQLite `received_via`. + * When `deliveryMethod` is set (Direct→PN fallback), patch `reticulumDeliveryMethod`. */ export function persistReticulumOutboundMessageStatus( identityId: IdentityId, @@ -125,13 +140,21 @@ export function persistReticulumOutboundMessageStatus( status: MessageStatus, errorMessage?: string, sentVia?: MessageTransport, + deliveryMethod?: MessageRecord['reticulumDeliveryMethod'], ): boolean { const before = useMessageStore.getState().messages[identityId]?.[messageId]; if (!before) return false; - // Do not regress a terminal Completes/Fails back to sending — still allow via patches. + // Do not regress a terminal Completes/Fails back to sending — still allow via/method patches. if (isTerminalStatus(before.status ?? 'sending') && status === 'sending') { - if (sentVia != null && sentVia !== before.receivedVia) { - const patched: MessageRecord = { ...before, receivedVia: sentVia }; + const viaChanged = sentVia != null && sentVia !== before.receivedVia; + const methodChanged = + deliveryMethod != null && deliveryMethod !== before.reticulumDeliveryMethod; + if (viaChanged || methodChanged) { + const patched: MessageRecord = { + ...before, + ...(viaChanged ? { receivedVia: sentVia } : {}), + ...(methodChanged ? { reticulumDeliveryMethod: deliveryMethod } : {}), + }; upsertMessage(identityId, patched); const senderHash = resolveOutboundSenderHash(patched); if (senderHash) { @@ -153,13 +176,21 @@ export function persistReticulumOutboundMessageStatus( status, ...(errorMessage !== undefined ? { error: errorMessage } : {}), }; + let patched = false; if (sentVia != null && sentVia !== record.receivedVia) { record = { ...record, receivedVia: sentVia }; + patched = true; + } + if (deliveryMethod != null && deliveryMethod !== record.reticulumDeliveryMethod) { + record = { ...record, reticulumDeliveryMethod: deliveryMethod }; + patched = true; + } + if (patched) { upsertMessage(identityId, record); } - // Intermediate sending without via change is already written on optimistic send. - if (status === 'sending' && sentVia == null) return true; - if (status === 'sending' && sentVia != null) { + // Intermediate sending without via/method change is already written on optimistic send. + if (status === 'sending' && sentVia == null && deliveryMethod == null) return true; + if (status === 'sending') { const senderHash = resolveOutboundSenderHash(record); if (senderHash) { persistReticulumOutboundRecord( @@ -188,6 +219,7 @@ export function persistReticulumOutboundMessageStatus( export interface ApplyReticulumOutboundDeliveryStatusOpts { sentVia?: string | null; + deliveryMethod?: string | null; } /** Apply sidecar Completes/Fails (and optional egress `sent_via`): store + SQLite. */ @@ -197,21 +229,42 @@ export function applyReticulumOutboundDeliveryStatus( wireStatus: string, opts?: ApplyReticulumOutboundDeliveryStatusOpts, ): void { + const normalizedHash = normalizeReticulumMessageHash(messageHash); + if (!isValidReticulumOutboundMessageHash(normalizedHash)) { + console.debug( + `[applyReticulumOutboundDeliveryStatus] drop invalid message_hash len=${normalizedHash.length}`, + ); + return; + } const status = mapLxmfOutboundWireStatus(wireStatus); + if (status == null) { + console.debug( + `[applyReticulumOutboundDeliveryStatus] drop unknown wire status=${wireStatus.slice(0, 32)}`, + ); + return; + } const sentVia = parseWireSentVia(opts?.sentVia); + const deliveryMethod = parseReticulumDeliveryMethod(opts?.deliveryMethod); const applied = persistReticulumOutboundMessageStatus( identityId, - messageHash, + normalizedHash, status, undefined, sentVia, + deliveryMethod, ); if (applied) { - pendingDeliveryByKey.delete(pendingDeliveryKey(identityId, messageHash)); + pendingDeliveryByKey.delete(pendingDeliveryKey(identityId, normalizedHash)); return; } - // Terminal status, or egress upgrade before rekey (sent_via with sending for later flush). - if (isTerminalStatus(status) || sentVia != null) { - bufferPendingDeliveryStatus(identityId, messageHash, wireStatus, opts?.sentVia ?? undefined); + // Terminal status, or egress/method upgrade before rekey for later flush. + if (isTerminalStatus(status) || sentVia != null || deliveryMethod != null) { + bufferPendingDeliveryStatus( + identityId, + normalizedHash, + wireStatus, + opts?.sentVia ?? undefined, + opts?.deliveryMethod ?? undefined, + ); } } diff --git a/src/renderer/lib/reticulum/reticulumMessageHash.ts b/src/renderer/lib/reticulum/reticulumMessageHash.ts index 21e0e2385..5ff2f9c0b 100644 --- a/src/renderer/lib/reticulum/reticulumMessageHash.ts +++ b/src/renderer/lib/reticulum/reticulumMessageHash.ts @@ -3,6 +3,15 @@ export function normalizeReticulumMessageHash(hash: string | null | undefined): return (hash ?? '').trim().toLowerCase(); } +/** + * Valid outbound WS / persist message hash: lowercase hex, 8–128 chars + * (LXMF hashes are 64 hex; provisional ids may be shorter hex). + */ +export function isValidReticulumOutboundMessageHash(hash: string | null | undefined): boolean { + const normalized = normalizeReticulumMessageHash(hash); + return /^[0-9a-f]{8,128}$/.test(normalized); +} + export function reticulumMessageHashesEqual( a: string | null | undefined, b: string | null | undefined, diff --git a/src/renderer/lib/reticulum/reticulumOutboundFailureBridge.test.ts b/src/renderer/lib/reticulum/reticulumOutboundFailureBridge.test.ts index 416af05e2..7b79b6471 100644 --- a/src/renderer/lib/reticulum/reticulumOutboundFailureBridge.test.ts +++ b/src/renderer/lib/reticulum/reticulumOutboundFailureBridge.test.ts @@ -77,4 +77,31 @@ describe('failReticulumSendingOutboundToDestHash', () => { }), ); }); + + it('skips outbound rows already on propagated (Direct→PN fallback)', () => { + const toNodeId = reticulumHashToNodeId(DEST); + registerReticulumDestinationHash(toNodeId, DEST); + useMessageStore.setState({ + messages: { + [identityId]: { + 'msg-hash': { + id: 'msg-hash', + from: 1, + senderName: 'self', + payload: 'hello', + channelIndex: 0, + timestamp: Date.now(), + status: 'sending', + to: toNodeId, + reticulumSenderHash: SELF, + reticulumDeliveryMethod: 'propagated', + }, + }, + }, + }); + + const count = failReticulumSendingOutboundToDestHash(identityId, DEST, 'link timeout'); + expect(count).toBe(0); + expect(useMessageStore.getState().messages[identityId]?.['msg-hash']?.status).toBe('sending'); + }); }); diff --git a/src/renderer/lib/reticulum/reticulumOutboundFailureBridge.ts b/src/renderer/lib/reticulum/reticulumOutboundFailureBridge.ts index 941c53fcc..754814661 100644 --- a/src/renderer/lib/reticulum/reticulumOutboundFailureBridge.ts +++ b/src/renderer/lib/reticulum/reticulumOutboundFailureBridge.ts @@ -35,6 +35,8 @@ export function failReticulumSendingOutboundToDestHash( let count = 0; for (const msg of Object.values(bucket)) { if (msg.status !== 'sending' || msg.to == null) continue; + // Direct→PN fallback re-queues as Propagated and emits sending — do not fail those rows. + if (msg.reticulumDeliveryMethod === 'propagated') continue; const destHash = resolveOutboundDestHash(msg.to); if (!destHash || !destHashMatchesPeer(destHash, targetNorm)) continue; if (persistReticulumOutboundMessageStatus(identityId, msg.id, 'failed', errorMessage)) { diff --git a/src/renderer/lib/reticulum/reticulumPropagationSync.ts b/src/renderer/lib/reticulum/reticulumPropagationSync.ts index 0ff00768f..343aa763d 100644 --- a/src/renderer/lib/reticulum/reticulumPropagationSync.ts +++ b/src/renderer/lib/reticulum/reticulumPropagationSync.ts @@ -6,12 +6,39 @@ export const RETICULUM_PROPAGATION_REFRESH_MIN_VISIBLE_MS = 500; /** Cancel sync when stuck establishing connection to an unreachable node. */ export const RETICULUM_PROPAGATION_SYNC_STALL_MS = 45_000; +/** How long a failed sync keeps the Diagnostics failing row visible. */ +export const RETICULUM_PROPAGATION_SYNC_FAILING_DIAGNOSTIC_TTL_MS = 60 * 60 * 1000; + /** Hard ceiling for any in-flight propagation sync (includes transfer). */ export const RETICULUM_PROPAGATION_SYNC_CEILING_MS = 180_000; /** Match sidecar Establishing (~10) — do not cancel once negotiation/transfer starts. */ export const RETICULUM_PROPAGATION_SYNC_ESTABLISHING_MAX_PROGRESS = 15; +export interface PropagationSyncStuckInput { + syncActive: boolean; + syncProgress: number; + lastAttemptAt: number | null; +} + +/** True while progress is still in the Establishing band. */ +export function isPropagationSyncStillEstablishing(progress: number): boolean { + return progress < RETICULUM_PROPAGATION_SYNC_ESTABLISHING_MAX_PROGRESS; +} + +/** True when sync is stuck in Establishing past the stall window. */ +export function isPropagationSyncEstablishingStuck( + input: PropagationSyncStuckInput, + now = Date.now(), +): boolean { + return ( + input.syncActive && + input.lastAttemptAt != null && + now - input.lastAttemptAt >= RETICULUM_PROPAGATION_SYNC_STALL_MS && + isPropagationSyncStillEstablishing(input.syncProgress) + ); +} + const SYNC_FAILED_KEY = 'reticulumPropagation.syncFailed'; const SYNC_TIMED_OUT_KEY = 'reticulumPropagation.syncTimedOut'; const SYNC_LOCAL_UNSUPPORTED_KEY = 'reticulumPropagation.syncLocalNotSupported'; @@ -111,7 +138,7 @@ export function schedulePropagationSyncStallWatchdog(): void { if (!sync.active) return; // Progress past Establishing means link+offer are in flight; lxmf-core owns the // remaining timeout (120s). Canceling here aborts healthy multi-hop syncs. - if (sync.progress >= RETICULUM_PROPAGATION_SYNC_ESTABLISHING_MAX_PROGRESS) { + if (!isPropagationSyncStillEstablishing(sync.progress)) { return; } void useReticulumPropagationStore.getState().cancelSync({ reasonKey: SYNC_TIMED_OUT_KEY }); diff --git a/src/renderer/lib/storeRecordAdapters.test.ts b/src/renderer/lib/storeRecordAdapters.test.ts index 20d655a6a..a029af82e 100644 --- a/src/renderer/lib/storeRecordAdapters.test.ts +++ b/src/renderer/lib/storeRecordAdapters.test.ts @@ -296,6 +296,20 @@ describe('store record adapters (merge precedence)', () => { expect(record.status).toBe('failed'); }); + it('rehydrates reticulumDeliveryMethod from DB delivery_method', () => { + const record = reticulumDbRowToMessageRecord({ + sender_id: 'aa'.repeat(16), + payload: 'hello', + timestamp: 1_700_000_000_000, + message_hash: 'cc'.repeat(16), + delivery_status: 'delivered', + delivery_method: 'propagated', + }); + expect(record.status).toBe('acked'); + expect(record.reticulumDeliveryMethod).toBe('propagated'); + expect(messageRecordToChatMessage(record).reticulumDeliveryMethod).toBe('propagated'); + }); + it('round-trips Reticulum LXMF hash and reply fields from DB rows', () => { const record = reticulumDbRowToMessageRecord({ sender_id: 'aa'.repeat(16), diff --git a/src/renderer/lib/storeRecordAdapters.ts b/src/renderer/lib/storeRecordAdapters.ts index 4c0be9305..3a16cd47c 100644 --- a/src/renderer/lib/storeRecordAdapters.ts +++ b/src/renderer/lib/storeRecordAdapters.ts @@ -4,6 +4,7 @@ import { MESHTASTIC_BROADCAST_NODE_NUM, } from '@/shared/nodeNameUtils'; import { MESHTASTIC_TAPBACK_DATA_EMOJI_FLAG } from '@/shared/reactionEmoji'; +import { parseReticulumDeliveryMethod } from '@/shared/reticulumDeliveryMethod'; import type { MessageRecord } from '../stores/messageStore'; import type { NodeRecord } from '../stores/nodeStore'; @@ -417,6 +418,7 @@ export function reticulumDbRowToMessageRecord(row: { message_hash?: string | null; received_via?: string | null; delivery_status?: string | null; + delivery_method?: string | null; attachment_path?: string | null; }): MessageRecord { const from = reticulumHashToNodeId(row.sender_id); @@ -432,6 +434,7 @@ export function reticulumDbRowToMessageRecord(row: { row.received_via === 'both' ? row.received_via : undefined; + const deliveryMethod = parseReticulumDeliveryMethod(row.delivery_method); const status: MessageRecord['status'] = row.delivery_status === 'failed' ? 'failed' @@ -457,6 +460,7 @@ export function reticulumDbRowToMessageRecord(row: { ? { reticulumReplyToHash: row.reply_to_hash } : {}), ...(receivedVia ? { receivedVia } : {}), + ...(deliveryMethod ? { reticulumDeliveryMethod: deliveryMethod } : {}), ...(row.attachment_path ? { reticulumAttachmentPath: row.attachment_path } : {}), }; } diff --git a/src/renderer/lib/types.ts b/src/renderer/lib/types.ts index 0b26ab592..ab0dc99ac 100644 --- a/src/renderer/lib/types.ts +++ b/src/renderer/lib/types.ts @@ -4,6 +4,7 @@ import { REGISTERED_MESH_PROTOCOLS, } from '@/shared/meshProtocol'; import type { MeshtasticLoraConfig } from '@/shared/meshtasticUrlEncoder'; +import type { ReticulumDeliveryMethod } from '@/shared/reticulumDeliveryMethod'; import type { TAKClientInfo, TAKServerStatus, TAKSettings } from '@/shared/tak-types'; export type { MeshProtocol }; @@ -375,8 +376,8 @@ export interface ChatMessage { reticulum_reply_to_hash?: string; /** Local path when a Reticulum attachment was saved to disk. */ reticulumAttachmentPath?: string; - /** Reticulum LXMF delivery method for outbound status badge (direct / propagated). */ - reticulumDeliveryMethod?: 'direct' | 'propagated' | 'opportunistic'; + /** Reticulum LXMF delivery method for outbound status badge (direct / propagated / opportunistic / paper). */ + reticulumDeliveryMethod?: ReticulumDeliveryMethod; sender_name: string; payload: string; channel: number; diff --git a/src/renderer/locales/cs/translation.json b/src/renderer/locales/cs/translation.json index 602a28188..a74a3dabc 100644 --- a/src/renderer/locales/cs/translation.json +++ b/src/renderer/locales/cs/translation.json @@ -218,7 +218,6 @@ "clearReticulumMessagesConfirmButton": "Vymazat {{count}} zprávy", "reticulumSection": "Reticulum — Zásobník retikula", "reticulumAnnounceHelp": "Jak často je vaše totožnost oznámena v síti a nástroje pro vymazání uložených oznámení.", - "reticulumPropagationHelp": "Zvolte způsob synchronizace propagačních uzlů a v případě potřeby vytáhněte nejnovější propagační data.", "logPanelHelp": "Je-li povoleno, na pravé straně se zobrazí živý záznam protokolu. Na kartě Reticulum řádky zařízení zahrnují výstup postranního vozíku a stav místního rozhraní. Řádky ladění vyžadují zaškrtávací políčko na panelu protokolu.", "autoPruneUnheardContactsDaysAria": "Automaticky oříznout neslyšené kontakty při spuštění, starší než {{days}} dnů", "capTotalContactsCountAria": "Omezit celkový počet kontaktů, ponechat naposledy zobrazené {{count}} kontakty", @@ -364,7 +363,8 @@ "shareLocationSendWaypointHint": "Pokud je povoleno, sdílení polohy Meshtastic posílá textovou zprávu i paket navigačních bodů, takže oficiální aplikace zobrazují špendlík na mapě. Chcete-li odesílat pouze text, vypněte u zaneprázdněných sítí.", "capStoredRrcMessages": "Omezit uloženou historii místností RRC, uchovat nejnovější", "capStoredRrcMessagesLabel": "Omezit uloženou historii místností RRC, uchovat nejnovější", - "capStoredRrcMessagesCountAria": "Omezit uloženou historii místností RRC, ponechat nejnovější zprávy {{count}}" + "capStoredRrcMessagesCountAria": "Omezit uloženou historii místností RRC, ponechat nejnovější zprávy {{count}}", + "reticulumPropagationHelp": "Nakonfigurujte vzdálené uzly šíření LXMF pro offline DM. Místní propagace je pouze schránka tohoto zařízení — nenahrazuje vzdálený uzel." }, "aria": { "closeDialog": "Zavřít dialog", @@ -613,7 +613,9 @@ "notificationDmTitle": "DM od {{sender}}", "notificationMessageTitle": "Zpráva od {{sender}}", "floodScopeOverrideUnscoped": "Unscoped", - "emojiButton": "Emoji" + "emojiButton": "Emoji", + "reticulumSendStoredAtPn": "Uloženo v propagačním uzlu", + "reticulumPnAbbrev": "PN" }, "chatPayload": { "mention": "Zmínit {{label}}", @@ -1374,7 +1376,8 @@ "potentialMeshcoreRepeaterConflict": "Potenciální konflikt MeshCore Repeater", "elevatedNoiseFloor": "Zvýšená hluková podlaha", "excessiveFlooding": "Nadměrné záplavy", - "reticulumTrafficDetected": "Zjištěn provoz Reticulum" + "reticulumTrafficDetected": "Zjištěn provoz Reticulum", + "highCompanionTxQueue": "Fronta High Companion TX" }, "rfCause": { "channelUtilizationSpike": "Aktuální {{current}}% je více než 2× nedávný průměr ({{average}}%) – možné přetížení nebo nárůst rušení.", @@ -1388,7 +1391,8 @@ "widebandNoiseFloor": "Širokopásmové rušení (vadná elektronika, šum ve vedení atd.) zvyšující hladinu hluku.", "fringeWeakCoverage": "Uzel je příliš daleko nebo je špatně připojen ke zbytku sítě.", "elevatedNoiseFloor": "Podlaha rádiového šumu je {{noiseFloor}} dBm — zvýšený efektivní rozsah snížení rušení.", - "excessiveFlooding": "{{percent}}% přenosů je zaplaveno — přímé směrování nemusí být vytvořeno s blízkými uzly." + "excessiveFlooding": "{{percent}}% přenosů je zaplaveno — přímé směrování nemusí být vytvořeno s blízkými uzly.", + "highCompanionTxQueue": "Hloubka doprovodné odchozí fronty je {{queueLen}}/256 — provoz může být zpožděn nebo vynechán." }, "foreignLoraCause": { "meshtastic": "Meshtastic uzel vysílající na této frekvenci. Odesílatel: {{sender}}. {{proximity}}Tento uzel se může nacházet v oblasti opakovače MeshCore.", @@ -1463,6 +1467,10 @@ "excessiveFlooding": { "title": "Povolit vytvoření přímých tras", "description": "Poměr zaplavení je velmi vysoký — počkejte, až se v uzlu nahromadí kontakty s přímou cestou, nebo zkontrolujte blízkost opakovače." + }, + "highCompanionTxQueue": { + "title": "Snižte odchozí provoz nebo zkontrolujte RF odkaz", + "description": "Doprovodná fronta TX je téměř plná — pozastavte odesílání, omezte floodování nebo zkontrolujte rušení a slabé RF spoje." } }, "cuHistoryHeading": "Využití kanálu (posledních 24 hodin)", @@ -1541,7 +1549,10 @@ "transportSaturatedShareInstance": "Nasycený transport RNS ({{count}} kapek s požadavkem na cestu). Sdílená instance je zapnutá — ukončete ostatní aplikace Reticulum (MeshChatX, Ratspeak, rnsd) a restartujte zásobník.", "slowTransportQuery": "Transportní dotazy RNS pomalé nebo selhávající ({{count}}×)", "bleBondRemoved": "Vazba BLE RNode „{{name}}“ je zastaralá – zapomeňte v nastavení Bluetooth v operačním systému a znovu spárujte", - "blePairingTimedOut": "Vypršel časový limit výměny klíčů ble RNode „{{name}}“ — zadejte aktuální PIN (ne 123456) a znovu spárujte" + "blePairingTimedOut": "Vypršel časový limit výměny klíčů ble RNode „{{name}}“ — zadejte aktuální PIN (ne 123456) a znovu spárujte", + "sidecarUnhealthy": "Postranní vozík Reticulum běží, ale nereaguje na zdravotní prohlídky", + "propagationSyncStuck": "Synchronizace propagačního uzlu se zasekla při vytváření odkazu", + "propagationSyncFailing": "Synchronizace propagačního uzlu se nezdařila" } }, "routingPort": { @@ -3955,7 +3966,11 @@ "discoveredHash": "{{hash}}", "syncEstablishNoLinkProof": "Synchronizace propagace nezískala žádný důkaz odkazu z uzlu — vaše identita může potřebovat nové oznámení nebo chybí cesta zpět. Zkuste Network → Announce nyní a poté Sync znovu.", "syncOfferUnknown": "Propagační uzel vrátil neznámou chybu nabídky synchronizace — zkuste to znovu za chvíli.", - "syncPathHint": "Vzdálená synchronizace vyžaduje cestu oběma směry. Synchronizace nejprve odešle krátké oznámení LXMF, aby mohl uzel odpovědět." + "syncPathHint": "Vzdálená synchronizace vyžaduje cestu oběma směry. Synchronizace nejprve odešle krátké oznámení LXMF, aby mohl uzel odpovědět.", + "setPreferredFailed": "Nelze nastavit preferovaný uzel šíření.", + "localInboxName": "Místní propagace (offline doručená pošta)", + "localInboxHint": "Místní propagace je offline doručená pošta tohoto zařízení. Neukládá odchozí DM pro nedosažitelné kolegy — nastavte vzdálený uzel lxmf.propagation jako preferovaný pro store-and-forward.", + "preferredLocalWarning": "Upřednostňovaný uzel je pouze místní — offline kolegové stále potřebují vzdálený propagační uzel pro doručování v obchodě." }, "reticulumPropagationHeader": { "modeLabel": "Režim propagace", diff --git a/src/renderer/locales/de/translation.json b/src/renderer/locales/de/translation.json index 9d264f743..7969a8a90 100644 --- a/src/renderer/locales/de/translation.json +++ b/src/renderer/locales/de/translation.json @@ -218,7 +218,6 @@ "clearReticulumMessagesConfirmButton": "{{count}} Nachrichten löschen", "reticulumSection": "Reticulum-Stack", "reticulumAnnounceHelp": "Wie oft Ihre Identität im Netzwerk bekannt gegeben wird, und Tools zum Löschen gespeicherter Ankündigungen.", - "reticulumPropagationHelp": "Wählen Sie aus, wie Ausbreitungsknoten synchronisiert werden, und ziehen Sie bei Bedarf die neuesten Ausbreitungsdaten ab.", "logPanelHelp": "Wenn diese Option aktiviert ist, wird rechts ein Live-Protokollstream angezeigt. Auf der Registerkarte „Reticulum“ umfassen die Gerätezeilen die Sidecar-Ausgabe und den Zustand der lokalen Schnittstelle. Für Debugzeilen ist das Kontrollkästchen im Protokollbereich erforderlich.", "autoPruneUnheardContactsDaysAria": "Ungehörte Kontakte, die älter als {{days}} Tage sind, werden beim Start automatisch bereinigt", "capTotalContactsCountAria": "Begrenzen Sie die Gesamtzahl der Kontakte und behalten Sie die zuletzt gesehenen {{count}}-Kontakte bei", @@ -364,7 +363,8 @@ "shareLocationSendWaypointHint": "Wenn aktiviert, senden Meshtastic-Standortfreigaben sowohl eine Textnachricht als auch ein Wegpunktpaket, sodass offizielle Apps eine Kartennadel anzeigen. Bei ausgelasteten Meshes deaktivieren, um nur Text zu senden.", "capStoredRrcMessages": "Gespeicherte RRC-Raumhistorie begrenzen, die neuesten behalten", "capStoredRrcMessagesLabel": "Gespeicherte RRC-Raumhistorie begrenzen, die neuesten behalten", - "capStoredRrcMessagesCountAria": "Begrenzen Sie den gespeicherten RRC-Raumverlauf und behalten Sie die neuesten {{count}}-Nachrichten bei" + "capStoredRrcMessagesCountAria": "Begrenzen Sie den gespeicherten RRC-Raumverlauf und behalten Sie die neuesten {{count}}-Nachrichten bei", + "reticulumPropagationHelp": "Konfigurieren Sie Remote-LXMF-Verbreitungsknoten für Offline-DMS. Die lokale Ausbreitung ist nur der Posteingang dieses Geräts — sie ersetzt keinen Remote-Knoten." }, "aria": { "closeDialog": "Schließen Dialog", @@ -611,7 +611,9 @@ "dmNodeHops_other": "{{count}} Hops", "notificationDmTitle": "DM von {{sender}}", "notificationMessageTitle": "Nachricht von {{sender}}", - "emojiButton": "Emojis" + "emojiButton": "Emojis", + "reticulumSendStoredAtPn": "Am Ausbreitungsknoten gespeichert", + "reticulumPnAbbrev": "PN" }, "chatPayload": { "mention": "Erwähne {{label}}", @@ -1372,7 +1374,8 @@ "potentialMeshcoreRepeaterConflict": "Potenzieller MeshCore-Repeater-Konflikt", "elevatedNoiseFloor": "Boden mit erhöhtem Geräuschpegel", "excessiveFlooding": "Übermäßige Überschwemmungen", - "reticulumTrafficDetected": "Reticulum-Verkehr erkannt" + "reticulumTrafficDetected": "Reticulum-Verkehr erkannt", + "highCompanionTxQueue": "High Companion TX-Warteschlange" }, "rfCause": { "channelUtilizationSpike": "Der aktuelle Wert von {{current}}% liegt über dem Zweifachen des aktuellen Durchschnitts ({{average}}%) — mögliche Stau- oder Störungsüberlastung.", @@ -1386,7 +1389,8 @@ "widebandNoiseFloor": "Breitbandige Störungen (fehlerhafte Elektronik, Stromleitungsgeräusche usw.), die den Geräuschpegel erhöhen.", "fringeWeakCoverage": "Der Knoten ist zu weit entfernt oder schlecht mit dem Rest des Netzes verbunden.", "elevatedNoiseFloor": "Der Funkgeräuschpegel beträgt {{noiseFloor}} dBm — erhöhte Interferenz reduziert die effektive Reichweite.", - "excessiveFlooding": "{{percent}}% der Übertragungen werden überflutet - ein direktes Routing mit Knoten in der Nähe ist möglicherweise nicht möglich." + "excessiveFlooding": "{{percent}}% der Übertragungen werden überflutet - ein direktes Routing mit Knoten in der Nähe ist möglicherweise nicht möglich.", + "highCompanionTxQueue": "Die Tiefe der ausgehenden Warteschlange des Begleiters beträgt {{queueLen}}/256 — der Datenverkehr kann sich verzögern oder fallen gelassen werden." }, "foreignLoraCause": { "meshtastic": "Meshtastic Knoten, der auf dieser Frequenz sendet. Absender: {{sender}}. {{proximity}}Dieser Knoten kann sich in deinem MeshCore-Repeater-Bereich befinden.", @@ -1461,6 +1465,10 @@ "excessiveFlooding": { "title": "Ermöglichen Sie die Einrichtung direkter Routen", "description": "Das Überschwemmungsverhältnis ist sehr hoch — warten Sie, bis der Knoten Direktpfadkontakte akkumuliert hat, oder überprüfen Sie die Nähe des Repeaters." + }, + "highCompanionTxQueue": { + "title": "Reduzieren Sie den ausgehenden Datenverkehr oder überprüfen Sie die HF-Verbindung", + "description": "Die Companion TX-Warteschlange ist fast voll — pausieren Sie das Senden, reduzieren Sie Überschwemmungen oder prüfen Sie auf Störungen und schwache Links." } }, "cuHistoryHeading": "Kanalauslastung (letzte 24h)", @@ -1539,7 +1547,10 @@ "transportSaturatedShareInstance": "RNS-Transport gesättigt ({{count}} Path-Request-Drops). Share-Instanz ist aktiviert — beenden Sie andere Reticulum-Apps (MeshChatX, Ratspeak, rnsd) und starten Sie den Stack neu.", "slowTransportQuery": "RNS-Transportabfragen langsam oder fehlgeschlagen ({{count}}×)", "bleBondRemoved": "Die BLE-RNode-Verbindung „{{name}}“ ist veraltet – vergessen Sie sie in den Bluetooth-Einstellungen des Betriebssystems und führen Sie erneut eine Kopplung durch", - "blePairingTimedOut": "BLE RNode \"{{name}}\" Passkey-Austausch abgelaufen — geben Sie die aktuelle PIN (nicht 123456) ein und koppeln Sie erneut" + "blePairingTimedOut": "BLE RNode \"{{name}}\" Passkey-Austausch abgelaufen — geben Sie die aktuelle PIN (nicht 123456) ein und koppeln Sie erneut", + "sidecarUnhealthy": "Reticulum Sidecar läuft, reagiert aber nicht auf Gesundheitschecks", + "propagationSyncStuck": "Die Synchronisierung des Ausbreitungsknotens bleibt hängen, um eine Verbindung herzustellen", + "propagationSyncFailing": "Synchronisierung des Ausbreitungsknotens fehlgeschlagen" } }, "routingPort": { @@ -3953,7 +3964,11 @@ "discoveredHash": "{{hash}}", "syncEstablishNoLinkProof": "Propagation Sync hat keinen Link-Beweis vom Knoten erhalten — Ihre Identität benötigt möglicherweise eine neue Ankündigung, oder der Pfad zurück fehlt. Versuchen Sie es jetzt mit Network → Announce und dann erneut mit Sync.", "syncOfferUnknown": "Der Ausbreitungsknoten hat einen unbekannten Sync-Angebotsfehler zurückgegeben — versuchen Sie es gleich noch einmal.", - "syncPathHint": "Die Fernsynchronisierung erfordert einen Pfad in beide Richtungen. Sync sendet zuerst eine kurze LXMF-Ankündigung, damit der Knoten antworten kann." + "syncPathHint": "Die Fernsynchronisierung erfordert einen Pfad in beide Richtungen. Sync sendet zuerst eine kurze LXMF-Ankündigung, damit der Knoten antworten kann.", + "setPreferredFailed": "Bevorzugter Ausbreitungsknoten konnte nicht festgelegt werden.", + "localInboxName": "Lokale Verbreitung (Offline-Posteingang)", + "localInboxHint": "Die lokale Verbreitung ist der Offline-Posteingang dieses Geräts. Es speichert keine ausgehenden DMs für nicht erreichbare Peers — legen Sie einen entfernten lxmf.propagation-Knoten als bevorzugt für Store-and-Forward fest.", + "preferredLocalWarning": "Bevorzugter Knoten ist nur lokal — Offline-Peers benötigen weiterhin einen Remote-Propagation-Knoten für die Store-and-Forward-Bereitstellung." }, "reticulumPropagationHeader": { "modeLabel": "Ausbreitungsmodus", diff --git a/src/renderer/locales/en/translation.json b/src/renderer/locales/en/translation.json index 9574c1b44..2cb39c41d 100644 --- a/src/renderer/locales/en/translation.json +++ b/src/renderer/locales/en/translation.json @@ -101,7 +101,7 @@ "floodAdvertSection": "Flood Advert", "reticulumSection": "Reticulum stack", "reticulumAnnounceHelp": "How often your identity is announced on the network (default 3600 s / 1 h when unset; 0 = startup-only), and tools to clear stored announces.", - "reticulumPropagationHelp": "Choose how propagation nodes are synced and pull the latest propagation data when needed.", + "reticulumPropagationHelp": "Configure remote LXMF propagation nodes for offline DMs. Local propagation is this device’s inbox only — it does not replace a remote node.", "floodAdvertScheduleLabel": "Automatically send a flood advert on a schedule:", "floodAdvertEvery12h": "Every 12 hours", "floodAdvertEvery24h": "Every 24 hours", @@ -468,6 +468,8 @@ "sentViaMultiple": "Sent via {{vias}}", "reticulumSendSending": "Sending…", "reticulumSendPropagated": "Queued at propagation node", + "reticulumSendStoredAtPn": "Stored at propagation node", + "reticulumPnAbbrev": "PN", "reticulumSendDelivered": "Delivered", "reticulumSendFailed": "Failed to send", "reticulumNoPropagationNode": "No propagation node configured. Set a preferred propagation node on the Reticulum Network tab.", @@ -1402,7 +1404,8 @@ "unknownLoraTraffic": "Unknown LoRa Traffic", "potentialMeshcoreRepeaterConflict": "Potential MeshCore Repeater Conflict", "elevatedNoiseFloor": "Elevated Noise Floor", - "excessiveFlooding": "Excessive Flooding" + "excessiveFlooding": "Excessive Flooding", + "highCompanionTxQueue": "High Companion TX Queue" }, "rfCause": { "channelUtilizationSpike": "Current {{current}}% is over 2× the recent average ({{average}}%) — possible congestion or interference surge.", @@ -1416,7 +1419,8 @@ "widebandNoiseFloor": "Broadband interference (faulty electronics, power-line noise, etc.) elevating the noise floor.", "fringeWeakCoverage": "Node is too far away or poorly connected to the rest of the mesh.", "elevatedNoiseFloor": "Radio noise floor is {{noiseFloor}} dBm — elevated interference reducing effective range.", - "excessiveFlooding": "{{percent}}% of transmissions are flood-routed — direct routing may not be established with nearby nodes." + "excessiveFlooding": "{{percent}}% of transmissions are flood-routed — direct routing may not be established with nearby nodes.", + "highCompanionTxQueue": "Companion outbound queue depth is {{queueLen}}/256 — traffic may be delayed or dropped." }, "foreignLoraCause": { "meshtastic": "Meshtastic node transmitting on this frequency. Sender: {{sender}}. {{proximity}}This node may be in your MeshCore repeater area.", @@ -1491,6 +1495,10 @@ "excessiveFlooding": { "title": "Allow direct routes to establish", "description": "Flood ratio is very high — wait for the node to accumulate direct-path contacts, or check repeater proximity." + }, + "highCompanionTxQueue": { + "title": "Reduce outbound traffic or check the RF link", + "description": "Companion TX queue is nearly full — pause sends, reduce floods, or check for interference and weak links." } }, "noDiagnosticsHealthy": "No diagnostics detected. The mesh looks healthy!", @@ -1540,7 +1548,10 @@ "slowTransportQuery": "RNS transport queries slow or failing ({{count}}×)", "noPeers": "No known peers in path table yet", "autoBeaconTunnelOnly": "AutoInterface beacon TX is failing on VPN tunnel interface(s) {{ifaces}}. Rebuild the Reticulum sidecar or temporarily disable AutoInterface if logs are flooded.", - "autoBeaconPhysicalFailures": "AutoInterface beacon TX is failing on {{ifaces}} — local LAN discovery may not work until this is fixed." + "autoBeaconPhysicalFailures": "AutoInterface beacon TX is failing on {{ifaces}} — local LAN discovery may not work until this is fixed.", + "sidecarUnhealthy": "Reticulum sidecar is running but not responding to health checks", + "propagationSyncStuck": "Propagation node sync is stuck establishing a link", + "propagationSyncFailing": "Propagation node sync failed" }, "audit": { "tcp_enable_key": "TCP interface \"{{name}}\" uses the wrong enable key — RNS will not load it.", @@ -4046,6 +4057,7 @@ }, "preferred": "Preferred", "setPreferred": "Set preferred", + "setPreferredFailed": "Could not set preferred propagation node.", "syncNow": "Sync messages", "syncNowFor": "Sync messages from {{name}}", "cancelSync": "Cancel sync", @@ -4086,6 +4098,9 @@ "online": "online" }, "localInboxStats": "{{count}} queued · {{bytes}} bytes", + "localInboxName": "Local propagation (offline inbox)", + "localInboxHint": "Local propagation is this device’s offline inbox. It does not store outbound DMs for unreachable peers — set a remote lxmf.propagation node as preferred for store-and-forward.", + "preferredLocalWarning": "Preferred node is Local only — offline peers still need a remote propagation node for store-and-forward delivery.", "refreshing": "Refreshing…", "refreshAria": "Refresh propagation nodes", "lastRefreshed": "Last updated {{time}}", diff --git a/src/renderer/locales/es/translation.json b/src/renderer/locales/es/translation.json index 7783b9ce4..cebb483d2 100644 --- a/src/renderer/locales/es/translation.json +++ b/src/renderer/locales/es/translation.json @@ -218,7 +218,6 @@ "clearReticulumMessagesConfirmButton": "Borrar {{count}} mensajes", "reticulumSection": "Reticulum — Pila de Reticulum", "reticulumAnnounceHelp": "Con qué frecuencia se anuncia su identidad en la red y herramientas para borrar los anuncios almacenados.", - "reticulumPropagationHelp": "Elija cómo se sincronizan los nodos de propagación y extraiga los últimos datos de propagación cuando sea necesario.", "logPanelHelp": "Cuando está habilitado, aparece una transmisión de registro en vivo a la derecha. En la pestaña Reticulum, las líneas de dispositivos incluyen la salida del sidecar y el estado de la interfaz local. Las líneas de depuración requieren la casilla de verificación dentro del panel de registro.", "autoPruneUnheardContactsDaysAria": "Eliminación automática de contactos no escuchados al inicio, con más de {{days}} días", "capTotalContactsCountAria": "Limitar el total de contactos, mantener los contactos {{count}} vistos más recientemente", @@ -364,7 +363,8 @@ "shareLocationSendWaypointHint": "Cuando están habilitados, los recursos compartidos de ubicación de Meshtastic envían un mensaje de texto y un paquete de Waypoint para que las aplicaciones oficiales muestren un marcador en el mapa. Desactívelo en mallas ocupadas para enviar solo texto.", "capStoredRrcMessages": "Limitar el historial de sala RRC almacenado, mantener el más reciente", "capStoredRrcMessagesLabel": "Limitar el historial de sala RRC almacenado, mantener el más reciente", - "capStoredRrcMessagesCountAria": "Limitar el historial de sala RRC almacenado, conservar los mensajes {{count}} más recientes" + "capStoredRrcMessagesCountAria": "Limitar el historial de sala RRC almacenado, conservar los mensajes {{count}} más recientes", + "reticulumPropagationHelp": "Configurar nodos de propagación LXMF remotos para DM sin conexión. La propagación local es solo la bandeja de entrada de este dispositivo: no reemplaza a un nodo remoto." }, "aria": { "closeDialog": "Cerrar cuadro de diálogo", @@ -611,7 +611,9 @@ "dmNodeHops_other": "{{count}} saltos", "notificationDmTitle": "DM desde {{sender}}", "notificationMessageTitle": "Mensaje de {{sender}}", - "emojiButton": "Emoji" + "emojiButton": "Emoji", + "reticulumSendStoredAtPn": "Almacenado en el nodo de propagación", + "reticulumPnAbbrev": "PN" }, "chatPayload": { "mention": "Mencionar {{label}}", @@ -1372,7 +1374,8 @@ "potentialMeshcoreRepeaterConflict": "Conflicto potencial del repetidor MeshCore", "elevatedNoiseFloor": "Piso de ruido elevado", "excessiveFlooding": "Inundaciones excesivas", - "reticulumTrafficDetected": "Tráfico Reticulum detectado" + "reticulumTrafficDetected": "Tráfico Reticulum detectado", + "highCompanionTxQueue": "Cola de TX de acompañamiento alto" }, "rfCause": { "channelUtilizationSpike": "El {{current}}% actual supera en más de 2veces la media reciente ({{average}}%) — posible congestión o aumento de interferencias.", @@ -1386,7 +1389,8 @@ "widebandNoiseFloor": "Interferencia de banda ancha (electrónica defectuosa, ruido de la línea eléctrica, etc.) que eleva el nivel de ruido.", "fringeWeakCoverage": "El nodo está demasiado lejos o mal conectado con el resto de la malla.", "elevatedNoiseFloor": "El nivel mínimo de ruido de radio es {{noiseFloor}} dBm — interferencia elevada que reduce el alcance efectivo.", - "excessiveFlooding": "{{percent}}% de las transmisiones están enrutadas por inundación: es posible que no se establezca un enrutamiento directo con los nodos cercanos." + "excessiveFlooding": "{{percent}}% de las transmisiones están enrutadas por inundación: es posible que no se establezca un enrutamiento directo con los nodos cercanos.", + "highCompanionTxQueue": "La profundidad de la cola de salida del acompañante es {{queueLen}}/256; es posible que el tráfico se retrase o se caiga." }, "foreignLoraCause": { "meshtastic": "Nodo Meshtastic que transmite en esta frecuencia. Remitente: {{sender}}. {{proximity}}Este nodo puede estar en tu zona de repetidores MeshCore.", @@ -1461,6 +1465,10 @@ "excessiveFlooding": { "title": "Permita que se establezcan rutas directas", "description": "La relación de inundación es muy alta: espere a que el nodo acumule contactos de trayectoria directa o compruebe la proximidad del repetidor." + }, + "highCompanionTxQueue": { + "title": "Reduce el tráfico saliente o comprueba el enlace de RF", + "description": "La cola de TX complementaria está casi llena: pausa los envíos, reduce las inundaciones o comprueba si hay interferencias y enlaces débiles." } }, "cuHistoryHeading": "Utilización del canal (últimas 24h)", @@ -1539,7 +1547,10 @@ "transportSaturatedShareInstance": "Transporte RNS saturado (caídas de solicitud de ruta {{count}}). La instancia de Share está activada: salga de otras aplicaciones de Reticulum (MeshChatX, Ratspeak, rnsd) y reinicie la pila.", "slowTransportQuery": "Consultas de transporte RNS lentas o fallidas ({{count}}×)", "bleBondRemoved": "El enlace BLE RNode \"{{name}}\" está obsoleto: olvídelo en la configuración de Bluetooth del sistema operativo y vuelva a emparejarlo", - "blePairingTimedOut": "Se agotó el tiempo de espera para el intercambio de clave de acceso de BLE RNode \"{{name}}\": ingrese el PIN actual (no 123456) y vuelva a emparejar" + "blePairingTimedOut": "Se agotó el tiempo de espera para el intercambio de clave de acceso de BLE RNode \"{{name}}\": ingrese el PIN actual (no 123456) y vuelva a emparejar", + "sidecarUnhealthy": "El sidecar de Reticulum está en ejecución pero no responde a las comprobaciones de estado", + "propagationSyncStuck": "La sincronización del nodo de propagación está atascada estableciendo un enlace", + "propagationSyncFailing": "Error en la sincronización del nodo de propagación" } }, "routingPort": { @@ -3953,7 +3964,11 @@ "discoveredHash": "{{hash}}", "syncEstablishNoLinkProof": "La sincronización de propagación no obtuvo ninguna prueba de enlace del nodo: es posible que su identidad necesite un nuevo anuncio o que falte la ruta de regreso. Prueba Network → Announce ahora y vuelve a sincronizar.", "syncOfferUnknown": "El nodo de propagación devolvió un error de oferta de sincronización desconocido. Inténtalo de nuevo en un momento.", - "syncPathHint": "La sincronización remota necesita una ruta en ambos sentidos. Sync envía primero un breve anuncio LXMF para que el nodo pueda responder." + "syncPathHint": "La sincronización remota necesita una ruta en ambos sentidos. Sync envía primero un breve anuncio LXMF para que el nodo pueda responder.", + "setPreferredFailed": "No se ha podido establecer el nodo de propagación preferido.", + "localInboxName": "Propagación local (bandeja de entrada sin conexión)", + "localInboxHint": "La propagación local es la bandeja de entrada sin conexión de este dispositivo. No almacena DM salientes para pares inalcanzables: establezca un nodo remoto lxmf.propagation como preferido para almacenar y reenviar.", + "preferredLocalWarning": "El nodo preferido es solo local: los compañeros sin conexión aún necesitan un nodo de propagación remoto para la entrega de almacenamiento y reenvío." }, "reticulumPropagationHeader": { "modeLabel": "modo de propagación", diff --git a/src/renderer/locales/fr/translation.json b/src/renderer/locales/fr/translation.json index bb3cb5696..646436d12 100644 --- a/src/renderer/locales/fr/translation.json +++ b/src/renderer/locales/fr/translation.json @@ -218,7 +218,6 @@ "clearReticulumMessagesConfirmButton": "Effacer les messages {{count}}", "reticulumSection": "Reticulum — Empilement de réticulum", "reticulumAnnounceHelp": "La fréquence à laquelle votre identité est annoncée sur le réseau et les outils pour effacer les annonces stockées.", - "reticulumPropagationHelp": "Choisissez comment les nœuds de propagation sont synchronisés et extrayez les dernières données de propagation si nécessaire.", "logPanelHelp": "Lorsqu'il est activé, un flux de journaux en direct apparaît sur la droite. Dans l’onglet Reticulum, les lignes de périphériques incluent la sortie side-car et l’état de l’interface locale. Les lignes de débogage nécessitent la case à cocher dans le panneau de journal.", "autoPruneUnheardContactsDaysAria": "Élague automatiquement les contacts non entendus au démarrage, datant de plus de {{days}} jours", "capTotalContactsCountAria": "Limiter le nombre total de contacts, conserver les contacts {{count}} les plus récemment vus", @@ -364,7 +363,8 @@ "shareLocationSendWaypointHint": "Lorsqu'ils sont activés, les partages de localisation Meshtastic envoient à la fois un message texte et un paquet Waypoint afin que les applications officielles affichent une épingle de carte. Désactivez-le sur les maillages occupés pour envoyer du texte uniquement.", "capStoredRrcMessages": "Limiter l'historique des salles RRC stockées, conserver les plus récentes", "capStoredRrcMessagesLabel": "Limiter l'historique des salles RRC stockées, conserver les plus récentes", - "capStoredRrcMessagesCountAria": "Limiter l'historique des salles RRC stockées, conserver les messages {{count}} les plus récents" + "capStoredRrcMessagesCountAria": "Limiter l'historique des salles RRC stockées, conserver les messages {{count}} les plus récents", + "reticulumPropagationHelp": "Configurez les nœuds de propagation LXMF distants pour les DM hors ligne. La propagation locale est la boîte de réception de cet appareil uniquement — elle ne remplace pas un nœud distant." }, "aria": { "closeDialog": "Fermer la boîte de dialogue", @@ -611,7 +611,9 @@ "dmNodeHops_other": "{{count}} sauts", "notificationDmTitle": "DM de {{sender}}", "notificationMessageTitle": "Message de {{sender}}", - "emojiButton": "Emoji" + "emojiButton": "Emoji", + "reticulumSendStoredAtPn": "Stocké au nœud de propagation", + "reticulumPnAbbrev": "PN" }, "chatPayload": { "mention": "Mention {{label}}", @@ -1372,7 +1374,8 @@ "potentialMeshcoreRepeaterConflict": "Conflit potentiel de répéteur MeshCore", "elevatedNoiseFloor": "Plancher de bruit surélevé", "excessiveFlooding": "Inondations excessives", - "reticulumTrafficDetected": "Trafic Reticulum détecté" + "reticulumTrafficDetected": "Trafic Reticulum détecté", + "highCompanionTxQueue": "File d'attente TX Companion élevée" }, "rfCause": { "channelUtilizationSpike": "Le {{current}}% actuel est supérieur à 2fois la moyenne récente ({{average}}%) — congestion ou interférence possible.", @@ -1386,7 +1389,8 @@ "widebandNoiseFloor": "Interférence à large bande (électronique défectueuse, bruit de ligne électrique, etc.) élevant le plancher de bruit.", "fringeWeakCoverage": "Le nœud est trop éloigné ou mal connecté au reste du maillage.", "elevatedNoiseFloor": "Le plancher de bruit radio est de {{noiseFloor}} dBm — interférence élevée réduisant la portée effective.", - "excessiveFlooding": "{{percent}}% des transmissions sont acheminées par inondation - le routage direct peut ne pas être établi avec les nœuds à proximité." + "excessiveFlooding": "{{percent}}% des transmissions sont acheminées par inondation - le routage direct peut ne pas être établi avec les nœuds à proximité.", + "highCompanionTxQueue": "La profondeur de la file d'attente sortante compagnon est de {{queueLen}}/256 — le trafic peut être retardé ou abandonné." }, "foreignLoraCause": { "meshtastic": "Nœud Meshtastic transmettant sur cette fréquence. Expéditeur : {{sender}}. {{proximity}}Ce nœud peut se trouver dans votre zone de répéteur MeshCore.", @@ -1461,6 +1465,10 @@ "excessiveFlooding": { "title": "Autoriser les itinéraires directs à établir", "description": "Le taux d'inondation est très élevé — attendez que le nœud accumule des contacts directs ou vérifiez la proximité du répéteur." + }, + "highCompanionTxQueue": { + "title": "Réduisez le trafic sortant ou vérifiez la liaison RF", + "description": "La file d'attente Companion TX est presque pleine — interrompez les envois, réduisez les inondations ou vérifiez les interférences et les liens faibles." } }, "cuHistoryHeading": "Utilisation des canaux (dernières 24 heures)", @@ -1539,7 +1547,10 @@ "transportSaturatedShareInstance": "RNS transport saturé ({{count}} path-request drops). L'instance de partage est activée — quittez les autres applications Reticulum (MeshChatX, Ratspeak, rnsd) et redémarrez la pile.", "slowTransportQuery": "Requêtes de transport RNS lentes ou en échec ({{count}}×)", "bleBondRemoved": "La liaison BLE RNode \"{{name}}\" est obsolète - Oubliez les paramètres Bluetooth du système d'exploitation et ré-appairez", - "blePairingTimedOut": "L'échange de clé ble RNode \"{{name}}\" a expiré — saisissez le code PIN actuel (pas 123456) et réappareillez" + "blePairingTimedOut": "L'échange de clé ble RNode \"{{name}}\" a expiré — saisissez le code PIN actuel (pas 123456) et réappareillez", + "sidecarUnhealthy": "Le sidecar Reticulum est en cours d’exécution mais ne répond pas aux contrôles de santé", + "propagationSyncStuck": "La synchronisation du nœud de propagation est bloquée pour établir un lien", + "propagationSyncFailing": "Échec de la synchronisation du nœud de propagation" } }, "routingPort": { @@ -3953,7 +3964,11 @@ "discoveredHash": "{{hash}}", "syncEstablishNoLinkProof": "La synchronisation de la propagation n'a reçu aucune preuve de lien du nœud — votre identité peut avoir besoin d'une nouvelle annonce ou le chemin de retour est manquant. Essayez Network → Announce maintenant, puis Sync à nouveau.", "syncOfferUnknown": "Le nœud de propagation a renvoyé une erreur d'offre de synchronisation inconnue — réessayez dans un instant.", - "syncPathHint": "La synchronisation à distance a besoin d'un chemin dans les deux sens. Sync envoie d'abord une courte annonce LXMF afin que le nœud puisse répondre." + "syncPathHint": "La synchronisation à distance a besoin d'un chemin dans les deux sens. Sync envoie d'abord une courte annonce LXMF afin que le nœud puisse répondre.", + "setPreferredFailed": "Impossible de définir le nœud de propagation préféré.", + "localInboxName": "Propagation locale (boîte de réception hors ligne)", + "localInboxHint": "La propagation locale est la boîte de réception hors ligne de cet appareil. Il ne stocke pas les DM sortants pour les pairs inaccessibles — définissez un nœud lxmf.propagation distant comme préféré pour le stockage et le transfert.", + "preferredLocalWarning": "Le nœud préféré est Local uniquement — les pairs hors ligne ont toujours besoin d'un nœud de propagation distant pour la livraison en stockage et en transfert." }, "reticulumPropagationHeader": { "modeLabel": "mode de propagation", diff --git a/src/renderer/locales/id/translation.json b/src/renderer/locales/id/translation.json index e78aa4328..d90680e4c 100644 --- a/src/renderer/locales/id/translation.json +++ b/src/renderer/locales/id/translation.json @@ -218,7 +218,6 @@ "clearReticulumMessagesConfirmButton": "Hapus pesan {{count}}", "reticulumSection": "Reticulum — Tumpukan Reticulum", "reticulumAnnounceHelp": "Seberapa sering identitas Anda diumumkan di jaringan, dan alat untuk menghapus pengumuman yang tersimpan.", - "reticulumPropagationHelp": "Pilih bagaimana node propagasi disinkronkan dan tarik data propagasi terbaru bila diperlukan.", "logPanelHelp": "Saat diaktifkan, streaming log langsung muncul di sebelah kanan. Pada tab Reticulum, jalur perangkat menyertakan keluaran sespan dan kesehatan antarmuka lokal. Baris debug memerlukan kotak centang di dalam panel log.", "autoPruneUnheardContactsDaysAria": "Pangkas otomatis kontak yang belum pernah terdengar saat startup, lebih lama dari {{days}} hari", "capTotalContactsCountAria": "Batasi total kontak, pertahankan kontak {{count}} yang terakhir dilihat", @@ -364,7 +363,8 @@ "shareLocationSendWaypointHint": "Saat diaktifkan, pembagian lokasi Meshtastic mengirimkan pesan teks dan paket Waypoint sehingga aplikasi resmi menampilkan pin peta. Nonaktifkan pada jerat sibuk untuk mengirim teks saja.", "capStoredRrcMessages": "Batasi riwayat ruang RRC yang disimpan, simpan yang terbaru", "capStoredRrcMessagesLabel": "Batasi riwayat ruang RRC yang disimpan, simpan yang terbaru", - "capStoredRrcMessagesCountAria": "Batasi riwayat ruang RRC yang disimpan, simpan pesan {{count}} terbaru" + "capStoredRrcMessagesCountAria": "Batasi riwayat ruang RRC yang disimpan, simpan pesan {{count}} terbaru", + "reticulumPropagationHelp": "Konfigurasikan node propagasi LXMF jarak jauh untuk DM offline. Propagasi lokal adalah kotak masuk perangkat ini saja — tidak menggantikan node jarak jauh." }, "aria": { "closeDialog": "Tutup dialog", @@ -611,7 +611,9 @@ "dmNodeHops_one": "{{count}} hop", "dmNodeHops_other": "{{count}} hop", "notificationDmTitle": "DM dari {{sender}}", - "notificationMessageTitle": "Pesan dari {{sender}}" + "notificationMessageTitle": "Pesan dari {{sender}}", + "reticulumSendStoredAtPn": "Disimpan di simpul propagasi", + "reticulumPnAbbrev": "PN" }, "chatPayload": { "mention": "Sebutkan {{label}}", @@ -1372,7 +1374,8 @@ "potentialMeshcoreRepeaterConflict": "Potensi Konflik Pengulang MeshCore", "elevatedNoiseFloor": "Lantai Kebisingan Tinggi", "excessiveFlooding": "Banjir Berlebihan", - "reticulumTrafficDetected": "Lalu lintas Reticulum terdeteksi" + "reticulumTrafficDetected": "Lalu lintas Reticulum terdeteksi", + "highCompanionTxQueue": "Antrean TX Pendamping Tinggi" }, "rfCause": { "channelUtilizationSpike": "{{current}}% saat ini lebih dari 2× rata-rata terkini ({{average}}%) — kemungkinan kemacetan atau lonjakan gangguan.", @@ -1386,7 +1389,8 @@ "widebandNoiseFloor": "Interferensi broadband (kerusakan elektronik, gangguan saluran listrik, dll.) meningkatkan tingkat kebisingan.", "fringeWeakCoverage": "Node terlalu jauh atau tidak terhubung dengan baik ke seluruh mesh.", "elevatedNoiseFloor": "Lantai kebisingan radio adalah {{noiseFloor}} dBm — gangguan tinggi yang mengurangi rentang efektif.", - "excessiveFlooding": "{{percent}}% transmisi dialiri banjir — perutean langsung mungkin tidak dilakukan dengan node terdekat." + "excessiveFlooding": "{{percent}}% transmisi dialiri banjir — perutean langsung mungkin tidak dilakukan dengan node terdekat.", + "highCompanionTxQueue": "Kedalaman antrean keluar pendamping adalah {{queueLen}}/256 — lalu lintas mungkin tertunda atau terputus." }, "foreignLoraCause": { "meshtastic": "Node Meshtastic mentransmisikan pada frekuensi ini. Pengirim: {{sender}}. {{proximity}}Node ini mungkin berada di area repeater MeshCore Anda.", @@ -1461,6 +1465,10 @@ "excessiveFlooding": { "title": "Izinkan rute langsung untuk ditetapkan", "description": "Rasio banjir sangat tinggi — tunggu hingga simpul menumpuk kontak jalur langsung, atau periksa kedekatan repeater." + }, + "highCompanionTxQueue": { + "title": "Kurangi lalu lintas keluar atau periksa tautan RF", + "description": "Antrean TX pendamping hampir penuh — jeda pengiriman, kurangi banjir, atau periksa gangguan dan tautan yang lemah." } }, "cuHistoryHeading": "Pemanfaatan Saluran (24 jam terakhir)", @@ -1539,7 +1547,10 @@ "transportSaturatedShareInstance": "Transportasi RNS jenuh ({{count}} permintaan jalur turun). Bagikan instance aktif — keluar dari aplikasi Reticulum lainnya (MeshChatX, Ratspeak, rnsd) dan mulai ulang tumpukan.", "slowTransportQuery": "Kueri transportasi RNS lambat atau gagal ({{count}}×)", "bleBondRemoved": "Ikatan BLE RNode \"{{name}}\" sudah basi — Lupakan pengaturan OS Bluetooth dan pasangkan kembali", - "blePairingTimedOut": "Waktu pertukaran kunci sandi BLE RNode \"{{name}}\" habis — masukkan PIN saat ini (bukan 123456) dan pasangkan kembali" + "blePairingTimedOut": "Waktu pertukaran kunci sandi BLE RNode \"{{name}}\" habis — masukkan PIN saat ini (bukan 123456) dan pasangkan kembali", + "sidecarUnhealthy": "Reticulum sidecar sedang berjalan tetapi tidak menanggapi pemeriksaan kesehatan", + "propagationSyncStuck": "Sinkronisasi simpul propagasi macet membuat tautan", + "propagationSyncFailing": "Sinkronisasi simpul propagasi gagal" } }, "routingPort": { @@ -3953,7 +3964,11 @@ "discoveredHash": "{{hash}}", "syncEstablishNoLinkProof": "Sinkronisasi propagasi tidak mendapatkan bukti tautan dari simpul — identitas Anda mungkin memerlukan pengumuman baru, atau jalur kembali hilang. Coba Network → Announce sekarang, lalu Sinkronkan lagi.", "syncOfferUnknown": "Simpul propagasi mengembalikan kesalahan penawaran sinkronisasi yang tidak diketahui — coba lagi sebentar lagi.", - "syncPathHint": "Sinkronisasi jarak jauh membutuhkan jalur dua arah. Sinkronisasi mengirimkan pengumuman LXMF singkat terlebih dahulu sehingga node dapat membalas." + "syncPathHint": "Sinkronisasi jarak jauh membutuhkan jalur dua arah. Sinkronisasi mengirimkan pengumuman LXMF singkat terlebih dahulu sehingga node dapat membalas.", + "setPreferredFailed": "Tidak dapat mengatur simpul propagasi yang diinginkan.", + "localInboxName": "Propagasi lokal (kotak masuk offline)", + "localInboxHint": "Propagasi lokal adalah kotak masuk offline perangkat ini. Ini tidak menyimpan DM keluar untuk rekan — rekan yang tidak dapat dijangkau — atur simpul lxmf.propagasi jarak jauh seperti yang disukai untuk store - and - forward.", + "preferredLocalWarning": "Simpul yang disukai hanya lokal — rekan offline masih membutuhkan simpul propagasi jarak jauh untuk pengiriman store - and - forward." }, "reticulumPropagationHeader": { "modeLabel": "Mode propagasi", diff --git a/src/renderer/locales/it/translation.json b/src/renderer/locales/it/translation.json index 0a3f3c8c4..1f37740c0 100644 --- a/src/renderer/locales/it/translation.json +++ b/src/renderer/locales/it/translation.json @@ -218,7 +218,6 @@ "clearReticulumMessagesConfirmButton": "Cancella messaggi {{count}}", "reticulumSection": "Reticulum — Stack Reticulum", "reticulumAnnounceHelp": "Con quale frequenza la tua identità viene annunciata sulla rete e gli strumenti per cancellare gli annunci memorizzati.", - "reticulumPropagationHelp": "Scegli come sincronizzare i nodi di propagazione e tira i dati di propagazione più recenti quando necessario.", "logPanelHelp": "Se abilitato, sulla destra viene visualizzato un flusso di registro in tempo reale. Nella scheda Reticulum, le righe del dispositivo includono l'output collaterale e l'integrità dell'interfaccia locale. Le righe di debug richiedono la casella di controllo all'interno del pannello di registro.", "autoPruneUnheardContactsDaysAria": "Elimina automaticamente i contatti non ascoltati all'avvio, più vecchi di {{days}} giorni", "capTotalContactsCountAria": "Limita i contatti totali, conserva i contatti {{count}} visti più di recente", @@ -364,7 +363,8 @@ "shareLocationSendWaypointHint": "Se abilitate, le condivisioni di posizione Meshtastic inviano sia un messaggio di testo che un pacchetto Waypoint in modo che le app ufficiali mostrino un segnaposto sulla mappa. Disabilita su mesh occupate per inviare solo testo.", "capStoredRrcMessages": "Limita la cronologia delle stanze RRC memorizzata e mantieni le più recenti", "capStoredRrcMessagesLabel": "Limita la cronologia delle stanze RRC memorizzata e mantieni le più recenti", - "capStoredRrcMessagesCountAria": "Limita la cronologia delle stanze RRC memorizzata e conserva i messaggi {{count}} più recenti" + "capStoredRrcMessagesCountAria": "Limita la cronologia delle stanze RRC memorizzata e conserva i messaggi {{count}} più recenti", + "reticulumPropagationHelp": "Configurare nodi di propagazione LXMF remoti per DM offline. La propagazione locale è solo la casella di posta in arrivo di questo dispositivo — non sostituisce un nodo remoto." }, "aria": { "closeDialog": "Chiudi finestra", @@ -611,7 +611,9 @@ "dmNodeHops_other": "{{count}} salti", "notificationDmTitle": "DM da {{sender}}", "notificationMessageTitle": "Messaggio da {{sender}}", - "emojiButton": "Emoji" + "emojiButton": "Emoji", + "reticulumSendStoredAtPn": "Memorizzato al nodo di propagazione", + "reticulumPnAbbrev": "PN" }, "chatPayload": { "mention": "Menziona {{label}}", @@ -1372,7 +1374,8 @@ "potentialMeshcoreRepeaterConflict": "Potenziale conflitto del ripetitore MeshCore", "elevatedNoiseFloor": "Pavimento Rumoroso Elevato", "excessiveFlooding": "Inondazioni eccessive", - "reticulumTrafficDetected": "Traffico Reticulum rilevato" + "reticulumTrafficDetected": "Traffico Reticulum rilevato", + "highCompanionTxQueue": "Coda TX High Companion" }, "rfCause": { "channelUtilizationSpike": "L'attuale {{current}}% è superiore a 2 volte la media recente ({{average}}%): possibile congestione o aumento di interferenze.", @@ -1386,7 +1389,8 @@ "widebandNoiseFloor": "Interferenze a banda larga (elettronica difettosa, rumore della linea elettrica, ecc.) che aumentano il rumore di fondo.", "fringeWeakCoverage": "Il nodo è troppo lontano o scarsamente connesso al resto della mesh.", "elevatedNoiseFloor": "Il rumore di fondo della radio è {{noiseFloor}} dBm — interferenza elevata che riduce la portata effettiva.", - "excessiveFlooding": "{{percent}}% delle trasmissioni sono instradate in modo alluvionale — l'instradamento diretto potrebbe non essere stabilito con nodi vicini." + "excessiveFlooding": "{{percent}}% delle trasmissioni sono instradate in modo alluvionale — l'instradamento diretto potrebbe non essere stabilito con nodi vicini.", + "highCompanionTxQueue": "La profondità della coda in uscita dell'accompagnatore è {{queueLen}}/256 — il traffico potrebbe subire ritardi o interruzioni." }, "foreignLoraCause": { "meshtastic": "Nodo Meshtastic che trasmette su questa frequenza. Mittente: {{sender}}. {{proximity}}Questo nodo potrebbe trovarsi nell'area del ripetitore MeshCore.", @@ -1461,6 +1465,10 @@ "excessiveFlooding": { "title": "Consenti l'istituzione di percorsi diretti", "description": "Il rapporto di inondazione è molto alto: attendere che il nodo accumuli contatti a percorso diretto o controllare la prossimità del ripetitore." + }, + "highCompanionTxQueue": { + "title": "Riduci il traffico in uscita o controlla il link RF", + "description": "La coda TX di Companion è quasi piena: metti in pausa gli invii, riduci le inondazioni o verifica la presenza di interferenze e collegamenti deboli." } }, "cuHistoryHeading": "Utilizzo del canale (ultime 24 ore)", @@ -1539,7 +1547,10 @@ "transportSaturatedShareInstance": "Trasporto RNS saturo ({{count}} gocce path-request). L'istanza di condivisione è attiva: esci da altre app Reticulum (MeshChatX, Ratspeak, rnsd) e riavvia lo stack.", "slowTransportQuery": "Query di trasporto RNS lente o non riuscite ({{count}}×)", "bleBondRemoved": "Il collegamento BLE RNode \"{{name}}\" è obsoleto: dimenticalo nelle impostazioni Bluetooth del sistema operativo e riassocialo", - "blePairingTimedOut": "Scambio passkey BLE RNode \"{{name}}\" scaduto: inserire il PIN corrente (non 123456) e associare nuovamente" + "blePairingTimedOut": "Scambio passkey BLE RNode \"{{name}}\" scaduto: inserire il PIN corrente (non 123456) e associare nuovamente", + "sidecarUnhealthy": "Il sidecar Reticulum è in esecuzione ma non risponde ai controlli di integrità", + "propagationSyncStuck": "La sincronizzazione del nodo di propagazione è bloccata stabilendo un collegamento", + "propagationSyncFailing": "Sincronizzazione del nodo di propagazione non riuscita" } }, "routingPort": { @@ -3953,7 +3964,11 @@ "discoveredHash": "{{hash}}", "syncEstablishNoLinkProof": "La sincronizzazione della propagazione non ha ricevuto alcuna prova di collegamento dal nodo: la tua identità potrebbe aver bisogno di un nuovo annuncio o manca il percorso di ritorno. Prova Network → Announce ora, quindi sincronizza di nuovo.", "syncOfferUnknown": "Il nodo di propagazione ha restituito un errore di offerta di sincronizzazione sconosciuto — riprova tra un momento.", - "syncPathHint": "La sincronizzazione remota richiede un percorso in entrambe le direzioni. Sync invia prima un breve annuncio LXMF in modo che il nodo possa rispondere." + "syncPathHint": "La sincronizzazione remota richiede un percorso in entrambe le direzioni. Sync invia prima un breve annuncio LXMF in modo che il nodo possa rispondere.", + "setPreferredFailed": "Impossibile impostare il nodo di propagazione preferito.", + "localInboxName": "Propagazione locale (casella di posta offline)", + "localInboxHint": "La propagazione locale è la casella di posta offline di questo dispositivo. Non memorizza i DM in uscita per i peer non raggiungibili: imposta un nodo lxmf.propagation remoto come preferito per lo store-and-forward.", + "preferredLocalWarning": "Il nodo preferito è solo locale: i peer offline hanno ancora bisogno di un nodo di propagazione remoto per la consegna store-and-forward." }, "reticulumPropagationHeader": { "modeLabel": "Modalità di propagazione", diff --git a/src/renderer/locales/ja/translation.json b/src/renderer/locales/ja/translation.json index cb6dcba7a..e9808f971 100644 --- a/src/renderer/locales/ja/translation.json +++ b/src/renderer/locales/ja/translation.json @@ -218,7 +218,6 @@ "clearReticulumMessagesConfirmButton": "{{count}}メッセージを消去", "reticulumSection": "Reticulum — レティキュラム・スタック", "reticulumAnnounceHelp": "あなたの身元がネットワーク上で発表される頻度と、保存されているアナウンスをクリアするためのツール。", - "reticulumPropagationHelp": "伝播ノードの同期方法を選択し、必要に応じて最新の伝播データを取得します。", "logPanelHelp": "有効にすると、ライブ ログ ストリームが右側に表示されます。 Reticulum タブのデバイス行には、サイドカー出力とローカル インターフェイスの状態が含まれます。デバッグ行では、ログ パネル内のチェックボックスが必要です。", "autoPruneUnheardContactsDaysAria": "起動時に、{{days}} 日より古い、聞いていない連絡先を自動的に削除します", "capTotalContactsCountAria": "連絡先の総数を制限し、最近確認された {{count}} 連絡先を保持します", @@ -364,7 +363,8 @@ "shareLocationSendWaypointHint": "有効にすると、Meshtastic 位置情報共有はテキスト メッセージとウェイポイント パケットの両方を送信するため、公式アプリにマップ ピンが表示されます。ビジーメッシュでテキストのみを送信するには無効にします。", "capStoredRrcMessages": "保存される RRC ルーム履歴に上限を設け、最新の状態に保つ", "capStoredRrcMessagesLabel": "保存される RRC ルーム履歴に上限を設け、最新の状態に保つ", - "capStoredRrcMessagesCountAria": "保存されている RRC ルーム履歴を制限し、最新の {{count}} メッセージを保持します" + "capStoredRrcMessagesCountAria": "保存されている RRC ルーム履歴を制限し、最新の {{count}} メッセージを保持します", + "reticulumPropagationHelp": "オフラインDMのリモートLXMF伝播ノードを設定します。ローカル伝播は、このデバイスの受信トレイのみであり、リモートノードを置き換えるものではありません。" }, "aria": { "closeDialog": "ダイアログを閉じる", @@ -611,7 +611,9 @@ "dmNodeHops_one": "{{count}} ホップ", "dmNodeHops_other": "{{count}} ホップ", "notificationDmTitle": "{{sender}}からのDM", - "notificationMessageTitle": "{{sender}}からのメッセージ" + "notificationMessageTitle": "{{sender}}からのメッセージ", + "reticulumSendStoredAtPn": "伝播ノードに格納されています", + "reticulumPnAbbrev": "PN" }, "chatPayload": { "mention": "{{label}} について言及してください", @@ -1372,7 +1374,8 @@ "potentialMeshcoreRepeaterConflict": "MeshCore リピーターの潜在的な競合", "elevatedNoiseFloor": "高所ノイズフロア", "excessiveFlooding": "過度の洪水", - "reticulumTrafficDetected": "Reticulumトラフィックを検出" + "reticulumTrafficDetected": "Reticulumトラフィックを検出", + "highCompanionTxQueue": "ハイコンパニオンTXキュー" }, "rfCause": { "channelUtilizationSpike": "現在の {{current}}% は最近の平均 ({{average}}%) の 2 倍を超えています。輻輳または干渉サージの可能性があります。", @@ -1386,7 +1389,8 @@ "widebandNoiseFloor": "広帯域干渉 (電子機器の故障、電力線ノイズなど) によりノイズ フロアが上昇します。", "fringeWeakCoverage": "ノードが遠すぎるか、メッシュの残りの部分との接続が不十分です。", "elevatedNoiseFloor": "ラジオノイズフロアは{{noiseFloor}} dBm — 有効範囲を低下させる高レベルの干渉。", - "excessiveFlooding": "送信の{{percent}}%はフラッドルーティング — 近隣ノードとの直接ルーティングが確立されていない可能性があります。" + "excessiveFlooding": "送信の{{percent}}%はフラッドルーティング — 近隣ノードとの直接ルーティングが確立されていない可能性があります。", + "highCompanionTxQueue": "コンパニオンのアウトバウンドキューの深さは{{queueLen}}/256です。トラフィックが遅延またはドロップされる可能性があります。" }, "foreignLoraCause": { "meshtastic": "Meshtastic ノードはこの周波数で送信します。送信者: {{sender}}。 {{proximity}}このノードは MeshCore リピーター エリア内にある可能性があります。", @@ -1461,6 +1465,10 @@ "excessiveFlooding": { "title": "直接ルートを確立できるようにする", "description": "フラッド比率は非常に高く、ノードが直接経路の接触を蓄積するのを待つか、リピーターの近接性を確認します。" + }, + "highCompanionTxQueue": { + "title": "アウトバウンドトラフィックを減らすか、RFリンクを確認してください", + "description": "コンパニオンTXキューはほぼいっぱいです。送信を一時停止したり、洪水を減らしたり、干渉や弱いリンクがないか確認したりできます。" } }, "cuHistoryHeading": "チャネル使用率(過去24時間)", @@ -1539,7 +1547,10 @@ "transportSaturatedShareInstance": "RNSトランスポートが飽和しました( {{count}}パス要求ドロップ)。共有インスタンスがオンになっています—他のReticulumアプリ( MeshChatX、Ratspeak、rnsd )を終了し、スタックを再起動します。", "slowTransportQuery": "RNSトランスポートクエリが遅いか失敗しました({{count}} ×)", "bleBondRemoved": "BLE RNode「{{name}}」のボンディングが古くなっています — OS の Bluetooth 設定でデバイスを削除して再ペアリングしてください", - "blePairingTimedOut": "BLE RNode \"{{name}}\" パスキー交換がタイムアウトしました — 現在の PIN (123456 ではない) を入力して、再ペアリングしてください" + "blePairingTimedOut": "BLE RNode \"{{name}}\" パスキー交換がタイムアウトしました — 現在の PIN (123456 ではない) を入力して、再ペアリングしてください", + "sidecarUnhealthy": "Reticulumサイドカーは実行中ですが、ヘルスチェックに応答していません", + "propagationSyncStuck": "伝播ノードの同期がリンクの確立に固執しています", + "propagationSyncFailing": "伝播ノードの同期に失敗しました" } }, "routingPort": { @@ -3953,7 +3964,11 @@ "discoveredHash": "{{hash}}", "syncEstablishNoLinkProof": "伝播同期では、ノードからリンクの証明が得られませんでした。IDには新しいアナウンスが必要な場合や、戻るパスが見つからない場合があります。今すぐネットワーク→アナウンスを試してから、再度同期してください。", "syncOfferUnknown": "伝搬ノードが不明な同期オファーエラーを返しました。しばらくしてからもう一度お試しください。", - "syncPathHint": "リモート同期には、両方向のパスが必要です。同期は、ノードが応答できるように、短いLXMFアナウンスを最初に送信します。" + "syncPathHint": "リモート同期には、両方向のパスが必要です。同期は、ノードが応答できるように、短いLXMFアナウンスを最初に送信します。", + "setPreferredFailed": "優先伝播ノードを設定できませんでした。", + "localInboxName": "ローカル伝播(オフライン受信トレイ)", + "localInboxHint": "ローカル伝播は、このデバイスのオフライン受信トレイです。到達不能なピアのアウトバウンドDMは保存されません。リモートlxmf.propagationノードをストアアンドフォワードに推奨するように設定します。", + "preferredLocalWarning": "優先ノードはローカルのみです。オフラインのピアは、ストアアンドフォワード配信のためにリモート伝播ノードを必要とします。" }, "reticulumPropagationHeader": { "modeLabel": "伝播モード", diff --git a/src/renderer/locales/ko/translation.json b/src/renderer/locales/ko/translation.json index 2fe77d5dd..70670f2ec 100644 --- a/src/renderer/locales/ko/translation.json +++ b/src/renderer/locales/ko/translation.json @@ -218,7 +218,6 @@ "clearReticulumMessagesConfirmButton": "메시지 {{count}} 개 지우기", "reticulumSection": "Reticulum — 레티큘럼 스택", "reticulumAnnounceHelp": "귀하의 신원이 네트워크에 공지되는 빈도 및 저장된 공지 사항을 지우는 도구.", - "reticulumPropagationHelp": "전파 노드의 동기화 방법을 선택하고 필요한 경우 최신 전파 데이터를 가져옵니다.", "logPanelHelp": "활성화되면 실시간 로그 스트림이 오른쪽에 나타납니다. Reticulum 탭의 장치 라인에는 사이드카 출력 및 로컬 인터페이스 상태가 포함됩니다. 디버그 라인에는 로그 패널 내부의 확인란이 필요합니다.", "autoPruneUnheardContactsDaysAria": "시작 시 {{days}}일 이상 경과한 확인되지 않은 연락처 자동 정리", "capTotalContactsCountAria": "총 연락처 수를 제한하고 가장 최근에 본 연락처를 {{count}}개로 유지합니다.", @@ -364,7 +363,8 @@ "shareLocationSendWaypointHint": "활성화되면 Meshtastic 위치 공유는 문자 메시지와 Waypoint 패킷을 모두 보내 공식 앱에 지도 핀이 표시됩니다. 바쁜 메시를 비활성화하면 텍스트만 보낼 수 있습니다.", "capStoredRrcMessages": "RRC 방 기록을 저장하고 최신 상태로 유지합니다.", "capStoredRrcMessagesLabel": "RRC 방 기록을 저장하고 최신 상태로 유지합니다.", - "capStoredRrcMessagesCountAria": "저장된 RRC 방 기록을 제한하고 최신 {{count}} 메시지를 유지합니다." + "capStoredRrcMessagesCountAria": "저장된 RRC 방 기록을 제한하고 최신 {{count}} 메시지를 유지합니다.", + "reticulumPropagationHelp": "오프라인 DM에 대한 원격 LXMF 전파 노드를 구성합니다. 로컬 전파는 이 장치의 받은 편지함일 뿐이며 원격 노드를 대체하지 않습니다." }, "aria": { "closeDialog": "대화상자 닫기", @@ -611,7 +611,9 @@ "dmNodeHops_one": "{{count}} 홉", "dmNodeHops_other": "{{count}} 홉", "notificationDmTitle": "{{sender}} 님의 DM", - "notificationMessageTitle": "{{sender}} 님의 메시지" + "notificationMessageTitle": "{{sender}} 님의 메시지", + "reticulumSendStoredAtPn": "전파 노드에 저장됨", + "reticulumPnAbbrev": "PN" }, "chatPayload": { "mention": "{{label}}을(를) 언급하세요", @@ -1372,7 +1374,8 @@ "potentialMeshcoreRepeaterConflict": "잠재적인 MeshCore 리피터 충돌", "elevatedNoiseFloor": "높은 소음층", "excessiveFlooding": "과도한 홍수", - "reticulumTrafficDetected": "Reticulum 트래픽 감지됨" + "reticulumTrafficDetected": "Reticulum 트래픽 감지됨", + "highCompanionTxQueue": "하이 컴패니언 TX 대기열" }, "rfCause": { "channelUtilizationSpike": "현재 {{current}}%는 최근 평균({{average}}%)의 2배를 초과합니다. 혼잡 또는 간섭 급증이 발생할 수 있습니다.", @@ -1386,7 +1389,8 @@ "widebandNoiseFloor": "광대역 간섭(전자 장치 결함, 전력선 소음 등)으로 인해 노이즈 플로어가 높아집니다.", "fringeWeakCoverage": "노드가 너무 멀리 떨어져 있거나 메시의 나머지 부분과 제대로 연결되어 있지 않습니다.", "elevatedNoiseFloor": "무선 노이즈 플로어는 {{noiseFloor}} dBm — 유효 범위를 줄이는 높은 간섭.", - "excessiveFlooding": "{{percent}}%의 전송이 플러드 라우팅됩니다 — 근처 노드에서 직접 라우팅이 설정되지 않을 수 있습니다." + "excessiveFlooding": "{{percent}}%의 전송이 플러드 라우팅됩니다 — 근처 노드에서 직접 라우팅이 설정되지 않을 수 있습니다.", + "highCompanionTxQueue": "동반자 아웃바운드 대기열 깊이는 {{queueLen}}/256입니다. 트래픽이 지연되거나 드롭될 수 있습니다." }, "foreignLoraCause": { "meshtastic": "이 주파수로 전송되는 Meshtastic 노드입니다. 발신자: {{sender}}. {{proximity}}이 노드는 MeshCore 리피터 영역에 있을 수 있습니다.", @@ -1461,6 +1465,10 @@ "excessiveFlooding": { "title": "직행 경로를 설정하도록 허용", "description": "홍수 비율이 매우 높습니다. 노드가 직접 경로 접촉을 축적하거나 리피터 근접성을 확인할 때까지 기다리십시오." + }, + "highCompanionTxQueue": { + "title": "아웃바운드 트래픽을 줄이거나 RF 링크를 확인하세요.", + "description": "Companion TX 대기열이 거의 가득 찼습니다. 전송을 일시 중지하거나, 홍수를 줄이거나, 간섭 및 약한 링크를 확인합니다." } }, "cuHistoryHeading": "채널 활용도 (마지막 24시간)", @@ -1539,7 +1547,10 @@ "transportSaturatedShareInstance": "RNS 전송 포화 ({{count}} 경로 요청 드롭). 인스턴스 공유 켜짐 — 다른 Reticulum 앱 (MeshChatX, Ratspeak, rnsd) 을 종료하고 스택을 다시 시작합니다.", "slowTransportQuery": "RNS 전송 쿼리가 느리거나 실패함 ({{count}} ×)", "bleBondRemoved": "BLE RNode \"{{name}}\" 본드가 오래되었습니다 — OS Bluetooth 설정에서 기기를 삭제하고 다시 페어링하세요", - "blePairingTimedOut": "BLE RNode \"{{name}}\" 암호 키 교환 시간이 초과되었습니다. 현재 PIN(123456 아님)을 입력하고 다시 페어링하세요." + "blePairingTimedOut": "BLE RNode \"{{name}}\" 암호 키 교환 시간이 초과되었습니다. 현재 PIN(123456 아님)을 입력하고 다시 페어링하세요.", + "sidecarUnhealthy": "Reticulum 사이드카가 작동 중이지만 상태 점검에 응답하지 않습니다", + "propagationSyncStuck": "전파 노드 동기화가 링크 설정 중 중단됨", + "propagationSyncFailing": "전파 노드 동기화 실패" } }, "routingPort": { @@ -3953,7 +3964,11 @@ "discoveredHash": "{{hash}}", "syncEstablishNoLinkProof": "전파 동기화는 노드에서 링크 증거를 얻지 못했습니다. 귀하의 신원은 새로운 공지가 필요하거나 경로가 누락되었을 수 있습니다. 지금 Network → Announce를 시도한 다음 다시 동기화하십시오.", "syncOfferUnknown": "전파 노드가 알 수 없는 동기화 제안 오류를 반환했습니다. 잠시 후 다시 시도하십시오.", - "syncPathHint": "원격 동기화는 양방향 경로가 필요합니다. Sync는 노드가 응답할 수 있도록 짧은 LXMF 알림을 먼저 보냅니다." + "syncPathHint": "원격 동기화는 양방향 경로가 필요합니다. Sync는 노드가 응답할 수 있도록 짧은 LXMF 알림을 먼저 보냅니다.", + "setPreferredFailed": "기본 전파 노드를 설정할 수 없습니다.", + "localInboxName": "로컬 전파 (오프라인 받은 편지함)", + "localInboxHint": "로컬 전파는 이 장치의 오프라인 받은 편지함입니다. 도달할 수 없는 피어를 위해 아웃바운드 DM을 저장하지 않습니다. 원격 lxmf.propagation 노드를 store-and-forward에 대해 선호하는 것으로 설정합니다.", + "preferredLocalWarning": "기본 설정 노드는 로컬 전용입니다. 오프라인 피어는 스토어 및 전달 전달을 위해 여전히 원격 전파 노드가 필요합니다." }, "reticulumPropagationHeader": { "modeLabel": "전파 모드", diff --git a/src/renderer/locales/nl/translation.json b/src/renderer/locales/nl/translation.json index 4e9d9c52b..fa0915e84 100644 --- a/src/renderer/locales/nl/translation.json +++ b/src/renderer/locales/nl/translation.json @@ -218,7 +218,6 @@ "clearReticulumMessagesConfirmButton": "{{count}} berichten wissen", "reticulumSection": "Reticulum stack", "reticulumAnnounceHelp": "Hoe vaak uw identiteit wordt aangekondigd op het netwerk en tools om opgeslagen aankondigingen te wissen.", - "reticulumPropagationHelp": "Kies hoe propagatieknooppunten worden gesynchroniseerd en haal de nieuwste propagatiegegevens op wanneer dat nodig is.", "logPanelHelp": "Indien ingeschakeld, verschijnt er aan de rechterkant een live logstream. Op het tabblad Reticulum omvatten apparaatregels zijspanuitvoer en lokale interfacestatus. Voor foutopsporingsregels is het selectievakje in het logpaneel vereist.", "autoPruneUnheardContactsDaysAria": "Ongehoorde contacten automatisch opschonen bij het opstarten, ouder dan {{days}} dagen", "capTotalContactsCountAria": "Beperk het totale aantal contacten, bewaar de meest recent geziene {{count}} contacten", @@ -364,7 +363,8 @@ "shareLocationSendWaypointHint": "Indien ingeschakeld, verzenden Meshtastic-locatieshares zowel een sms-bericht als een Waypoint-pakket, zodat officiële apps een kaartspeld tonen. Schakel uit bij drukke meshes om alleen tekst te verzenden.", "capStoredRrcMessages": "Beperk opgeslagen RRC-ruimtegeschiedenis, bewaar de nieuwste", "capStoredRrcMessagesLabel": "Beperk opgeslagen RRC-ruimtegeschiedenis, bewaar de nieuwste", - "capStoredRrcMessagesCountAria": "Beperk de opgeslagen RRC-ruimtegeschiedenis, bewaar de nieuwste {{count}}-berichten" + "capStoredRrcMessagesCountAria": "Beperk de opgeslagen RRC-ruimtegeschiedenis, bewaar de nieuwste {{count}}-berichten", + "reticulumPropagationHelp": "Configureer externe LXMF-voortplantingsknooppunten voor offline DM's. Lokale verspreiding is alleen de inbox van dit apparaat — het vervangt geen extern knooppunt." }, "aria": { "closeDialog": "Dialoogvenster sluiten", @@ -611,7 +611,9 @@ "notificationDmTitle": "DM vanaf {{sender}}", "notificationMessageTitle": "Bericht van {{sender}}", "emojiButton": "Emoji", - "openDmByAddress": "Open" + "openDmByAddress": "Open", + "reticulumSendStoredAtPn": "Opgeslagen op propagatieknooppunt", + "reticulumPnAbbrev": "PN" }, "chatPayload": { "mention": "Vermeld {{label}}", @@ -1372,7 +1374,8 @@ "potentialMeshcoreRepeaterConflict": "Potentieel MeshCore Repeater-conflict", "elevatedNoiseFloor": "Verhoogde geluidsvloer", "excessiveFlooding": "Overmatige overstromingen", - "reticulumTrafficDetected": "Reticulum-verkeer gedetecteerd" + "reticulumTrafficDetected": "Reticulum-verkeer gedetecteerd", + "highCompanionTxQueue": "High Companion TX-wachtrij" }, "rfCause": { "channelUtilizationSpike": "De huidige {{current}}% is meer dan 2x het recente gemiddelde ({{average}}%) – mogelijke congestie of interferentiepiek.", @@ -1386,7 +1389,8 @@ "widebandNoiseFloor": "Breedbandinterferentie (defecte elektronica, ruis op het elektriciteitsnet, enz.) waardoor de ruisvloer hoger wordt.", "fringeWeakCoverage": "Het knooppunt is te ver weg of slecht verbonden met de rest van de mesh.", "elevatedNoiseFloor": "Radiogeluidsvloer is {{noiseFloor}} dBm — verhoogde interferentie die het effectieve bereik vermindert.", - "excessiveFlooding": "{{percent}}% van de transmissies wordt via een overstromingsrouter geleid. Het is mogelijk dat er geen directe routering is met knooppunten in de buurt." + "excessiveFlooding": "{{percent}}% van de transmissies wordt via een overstromingsrouter geleid. Het is mogelijk dat er geen directe routering is met knooppunten in de buurt.", + "highCompanionTxQueue": "De uitgaande wachtrijdiepte van de begeleider is {{queueLen}}/256. Het verkeer kan vertraagd of vertraagd zijn." }, "foreignLoraCause": { "meshtastic": "Meshtastic knooppunt dat op deze frequentie zendt. Afzender: {{sender}}. {{proximity}}Dit knooppunt bevindt zich mogelijk in uw MeshCore-repeatergebied.", @@ -1461,6 +1465,10 @@ "excessiveFlooding": { "title": "Laat directe routes tot stand komen", "description": "Overstromingsverhouding is zeer hoog — wacht tot het knooppunt directe padcontacten verzamelt of controleer de nabijheid van de repeater." + }, + "highCompanionTxQueue": { + "title": "Verminder uitgaand verkeer of controleer de RF-link", + "description": "De wachtrij van Companion TX is bijna vol — pauzeer verzendingen, verminder overstromingen of controleer op interferentie en zwakke schakels." } }, "cuHistoryHeading": "Kanaalgebruik (laatste 24 uur)", @@ -1539,7 +1547,10 @@ "transportSaturatedShareInstance": "RNS-transport verzadigd ({{count}} pad-aanvraagdruppels). Deel instantie is ingeschakeld — sluit andere Reticulum-apps (MeshChatX, Ratspeak, rnsd) en start de stapel opnieuw.", "slowTransportQuery": "RNS-transportvragen traag of mislukt ({{count}}×)", "bleBondRemoved": "BLE RNode \"{{name}}\"-verbinding is verouderd: vergeet de Bluetooth-instellingen van het besturingssysteem en koppel opnieuw", - "blePairingTimedOut": "Er is een time-out opgetreden bij het uitwisselen van de BLE RNode \"{{name}}\"-wachtwoord: voer de huidige pincode in (niet 123456) en koppel opnieuw" + "blePairingTimedOut": "Er is een time-out opgetreden bij het uitwisselen van de BLE RNode \"{{name}}\"-wachtwoord: voer de huidige pincode in (niet 123456) en koppel opnieuw", + "sidecarUnhealthy": "Reticulum-zijspan loopt, maar reageert niet op gezondheidscontroles", + "propagationSyncStuck": "Synchronisatie van propagatieknooppunt zit vast bij het tot stand brengen van een link", + "propagationSyncFailing": "Synchronisatie van propagatieknooppunt mislukt" } }, "routingPort": { @@ -3953,7 +3964,11 @@ "discoveredHash": "{{hash}}", "syncEstablishNoLinkProof": "Voortplantingssynchronisatie heeft geen linkbewijs van het knooppunt gekregen — uw identiteit heeft mogelijk een nieuwe aankondiging nodig of het pad terug ontbreekt. Probeer Network → Announce nu en vervolgens Sync opnieuw.", "syncOfferUnknown": "Het propagatieknooppunt heeft een onbekende synchronisatiefout geretourneerd — probeer het zo meteen opnieuw.", - "syncPathHint": "Synchronisatie op afstand vereist een pad in beide richtingen. Sync stuurt eerst een korte LXMF-aankondiging zodat het knooppunt kan antwoorden." + "syncPathHint": "Synchronisatie op afstand vereist een pad in beide richtingen. Sync stuurt eerst een korte LXMF-aankondiging zodat het knooppunt kan antwoorden.", + "setPreferredFailed": "Kan voorkeursvoortplantingsknooppunt niet instellen.", + "localInboxName": "Lokale verspreiding (offline inbox)", + "localInboxHint": "Lokale verspreiding is de offline inbox van dit apparaat. Het slaat geen uitgaande DM's op voor onbereikbare collega's — stel een extern lxmf.propagation-knooppunt in als voorkeur voor opslaan en doorsturen.", + "preferredLocalWarning": "Het voorkeursknooppunt is alleen lokaal — offline collega's hebben nog steeds een extern propagatieknooppunt nodig voor store-and-forward-levering." }, "reticulumPropagationHeader": { "modeLabel": "voortplantingsmodus", diff --git a/src/renderer/locales/pl/translation.json b/src/renderer/locales/pl/translation.json index 407408281..07ea34a06 100644 --- a/src/renderer/locales/pl/translation.json +++ b/src/renderer/locales/pl/translation.json @@ -218,7 +218,6 @@ "clearReticulumMessagesConfirmButton": "Wyczyść wiadomości: {{count}}", "reticulumSection": "Reticulum — Stos siateczkowy", "reticulumAnnounceHelp": "Jak często Twoja tożsamość jest ogłaszana w sieci i narzędzia do czyszczenia przechowywanych ogłoszeń.", - "reticulumPropagationHelp": "Wybierz sposób synchronizacji węzłów propagacji i w razie potrzeby pobierz najnowsze dane propagacji.", "logPanelHelp": "Po włączeniu po prawej stronie pojawia się strumień dziennika na żywo. Na karcie Reticulum linie urządzeń obejmują wyjście wózka bocznego i stan interfejsu lokalnego. Linie debugowania wymagają pola wyboru w panelu dziennika.", "autoPruneUnheardContactsDaysAria": "Automatycznie usuwaj niesłyszane kontakty przy uruchomieniu, starsze niż {{days}} dni", "capTotalContactsCountAria": "Ogranicz wszystkie kontakty, zachowaj ostatnio widziane kontakty {{count}}", @@ -364,7 +363,8 @@ "shareLocationSendWaypointHint": "Po włączeniu udostępnianie lokalizacji Meshtastic wysyła zarówno wiadomość tekstową, jak i pakiet Waypoint, dzięki czemu oficjalne aplikacje pokazują pinezkę na mapie. Wyłącz w przypadku zajętych sieci, aby wysyłać tylko tekst.", "capStoredRrcMessages": "Ogranicz zapisaną historię pokoi RRC, zachowaj najnowsze", "capStoredRrcMessagesLabel": "Ogranicz zapisaną historię pokoi RRC, zachowaj najnowsze", - "capStoredRrcMessagesCountAria": "Ogranicz zapisaną historię pokoju RRC, zachowaj najnowsze wiadomości {{count}}" + "capStoredRrcMessagesCountAria": "Ogranicz zapisaną historię pokoju RRC, zachowaj najnowsze wiadomości {{count}}", + "reticulumPropagationHelp": "Skonfiguruj zdalne węzły propagacji LXMF dla wiadomości DM offline. Lokalna propagacja to tylko skrzynka odbiorcza tego urządzenia — nie zastępuje zdalnego węzła." }, "aria": { "closeDialog": "Zamknij okno dialogowe", @@ -613,7 +613,9 @@ "dmNodeHops_other": "{{count}} przeskoku", "notificationDmTitle": "Wiadomość prywatna od {{sender}}", "notificationMessageTitle": "Wiadomość od {{sender}}", - "emojiButton": "Emoji" + "emojiButton": "Emoji", + "reticulumSendStoredAtPn": "Przechowywane w węźle propagacji", + "reticulumPnAbbrev": "PN" }, "chatPayload": { "mention": "Wspomnij o {{label}}", @@ -1374,7 +1376,8 @@ "potentialMeshcoreRepeaterConflict": "Potencjalny konflikt wzmacniaków MeshCore", "elevatedNoiseFloor": "Podwyższona podłoga hałasu", "excessiveFlooding": "Nadmierne powodzie", - "reticulumTrafficDetected": "Wykryto ruch Reticulum" + "reticulumTrafficDetected": "Wykryto ruch Reticulum", + "highCompanionTxQueue": "Kolejka Teksasu High Companion" }, "rfCause": { "channelUtilizationSpike": "Prąd {{current}}% jest ponad 2 razy większy od ostatniej średniej ({{average}}%) — możliwe przeciążenie lub wzrost zakłóceń.", @@ -1388,7 +1391,8 @@ "widebandNoiseFloor": "Zakłócenia szerokopasmowe (wadliwa elektronika, zakłócenia w linii energetycznej itp.) podnoszące poziom szumów.", "fringeWeakCoverage": "Węzeł jest zbyt daleko lub słabo połączony z resztą siatki.", "elevatedNoiseFloor": "Poziom szumów radiowych wynosi {{noiseFloor}} dBm — podwyższone zakłócenia zmniejszające skuteczny zasięg.", - "excessiveFlooding": "{{percent}}% transmisji jest kierowanych przez powódź — trasowanie bezpośrednie może nie zostać ustanowione z pobliskimi węzłami." + "excessiveFlooding": "{{percent}}% transmisji jest kierowanych przez powódź — trasowanie bezpośrednie może nie zostać ustanowione z pobliskimi węzłami.", + "highCompanionTxQueue": "Głębokość kolejki wychodzącej towarzyszącej wynosi {{queueLen}}/256 — ruch może być opóźniony lub porzucony." }, "foreignLoraCause": { "meshtastic": "Węzeł Meshtastic nadający na tej częstotliwości. Nadawca: {{sender}}. {{proximity}}Ten węzeł może znajdować się w obszarze wzmacniacza MeshCore.", @@ -1463,6 +1467,10 @@ "excessiveFlooding": { "title": "Zezwalaj na bezpośrednie wyznaczanie tras", "description": "Wskaźnik powodzi jest bardzo wysoki — poczekaj, aż węzeł zgromadzi styki ścieżki bezpośredniej lub sprawdź bliskość przemiennika." + }, + "highCompanionTxQueue": { + "title": "Zmniejsz ruch wychodzący lub sprawdź łącze RF", + "description": "Kolejka nadajnika towarzyszącego jest prawie pełna — pauza wysyła, zmniejsza powodzie lub sprawdza zakłócenia i słabe ogniwa." } }, "cuHistoryHeading": "Wykorzystanie kanału (ostatnie 24 godziny)", @@ -1541,7 +1549,10 @@ "transportSaturatedShareInstance": "Transport RNS nasycony (krople z żądaniem ścieżki {{count}}). Udostępnij instancję jest włączona — zamknij inne aplikacje Reticulum (MeshChatX, Ratspeak, rnsd) i uruchom ponownie stos.", "slowTransportQuery": "Powolne lub nieudane zapytania transportowe RNS ({{count}}×)", "bleBondRemoved": "Wiązanie BLE RNode „{{name}}” jest nieaktualne — usuń urządzenie w ustawieniach Bluetooth systemu i sparuj ponownie", - "blePairingTimedOut": "Upłynął limit czasu wymiany hasła BLE RNode „{{name}}” — wprowadź bieżący kod PIN (nie 123456) i sparuj ponownie" + "blePairingTimedOut": "Upłynął limit czasu wymiany hasła BLE RNode „{{name}}” — wprowadź bieżący kod PIN (nie 123456) i sparuj ponownie", + "sidecarUnhealthy": "Sidecar Reticulum działa, ale nie odpowiada na kontrole stanu", + "propagationSyncStuck": "Synchronizacja węzła propagacji utknęła, tworząc łącze", + "propagationSyncFailing": "Synchronizacja węzła propagacji nie powiodła się" } }, "routingPort": { @@ -3955,7 +3966,11 @@ "discoveredHash": "{{hash}}", "syncEstablishNoLinkProof": "Synchronizacja propagacji nie uzyskała żadnego dowodu linku z węzła — Twoja tożsamość może wymagać nowego ogłoszenia lub brakuje ścieżki powrotnej. Wypróbuj Network → Announce teraz, a następnie ponownie Sync.", "syncOfferUnknown": "Węzeł propagacji zwrócił nieznany błąd oferty synchronizacji — spróbuj ponownie za chwilę.", - "syncPathHint": "Zdalna synchronizacja wymaga ścieżki w obie strony. SYNC najpierw wysyła krótkie ogłoszenie LXMF, aby węzeł mógł odpowiedzieć." + "syncPathHint": "Zdalna synchronizacja wymaga ścieżki w obie strony. SYNC najpierw wysyła krótkie ogłoszenie LXMF, aby węzeł mógł odpowiedzieć.", + "setPreferredFailed": "Nie można ustawić preferowanego węzła propagacji.", + "localInboxName": "Lokalna propagacja (skrzynka odbiorcza offline)", + "localInboxHint": "Lokalna propagacja to skrzynka odbiorcza offline tego urządzenia. Nie przechowuje wychodzących wiadomości DM dla nieosiągalnych rówieśników — ustaw zdalny węzeł lxmf.propagation jako preferowany dla store-and-forward.", + "preferredLocalWarning": "Preferowany węzeł jest tylko lokalny — rówieśnicy offline nadal potrzebują zdalnego węzła propagacji do dostarczania do magazynu i do przodu." }, "reticulumPropagationHeader": { "modeLabel": "Tryb propagacji:", diff --git a/src/renderer/locales/pt-BR/translation.json b/src/renderer/locales/pt-BR/translation.json index fc8697c8c..685fdc0e8 100644 --- a/src/renderer/locales/pt-BR/translation.json +++ b/src/renderer/locales/pt-BR/translation.json @@ -218,7 +218,6 @@ "clearReticulumMessagesConfirmButton": "Limpar mensagens {{count}}", "reticulumSection": "Reticulum — Pilha de Reticulum", "reticulumAnnounceHelp": "Com que frequência sua identidade é anunciada na rede e ferramentas para limpar anúncios armazenados.", - "reticulumPropagationHelp": "Escolha como os nós de propagação são sincronizados e extraia os dados de propagação mais recentes quando necessário.", "logPanelHelp": "Quando ativado, um fluxo de log ao vivo aparece à direita. Na guia Reticulum, as linhas do dispositivo incluem saída secundária e integridade da interface local. As linhas de depuração exigem a caixa de seleção dentro do painel de log.", "autoPruneUnheardContactsDaysAria": "Remover automaticamente contatos não ouvidos na inicialização, com mais de {{days}} dias", "capTotalContactsCountAria": "Limite o total de contatos, mantenha os contatos {{count}} vistos mais recentemente", @@ -364,7 +363,8 @@ "shareLocationSendWaypointHint": "Quando ativado, os compartilhamentos de localização do Meshtastic enviam uma mensagem de texto e um pacote de waypoint para que os aplicativos oficiais mostrem um alfinete no mapa. Desative em malhas ocupadas para enviar apenas texto.", "capStoredRrcMessages": "Limite o histórico da sala RRC armazenado, mantenha o mais recente", "capStoredRrcMessagesLabel": "Limite o histórico da sala RRC armazenado, mantenha o mais recente", - "capStoredRrcMessagesCountAria": "Limite o histórico da sala RRC armazenado, mantenha as mensagens {{count}} mais recentes" + "capStoredRrcMessagesCountAria": "Limite o histórico da sala RRC armazenado, mantenha as mensagens {{count}} mais recentes", + "reticulumPropagationHelp": "Configurar nós de propagação LXMF remotos para DMs offline. A propagação local é apenas a caixa de entrada deste dispositivo — ela não substitui um nó remoto." }, "aria": { "closeDialog": "Fechar janela", @@ -611,7 +611,9 @@ "dmNodeHops_one": "{{count}} hop", "dmNodeHops_other": "{{count}} saltos", "notificationDmTitle": "DM de {{sender}}", - "notificationMessageTitle": "Mensagem de {{sender}}" + "notificationMessageTitle": "Mensagem de {{sender}}", + "reticulumSendStoredAtPn": "Armazenado no nó de propagação", + "reticulumPnAbbrev": "PN" }, "chatPayload": { "mention": "Mencionar {{label}}", @@ -1372,7 +1374,8 @@ "potentialMeshcoreRepeaterConflict": "Potencial Conflito do Repetidor MeshCore", "elevatedNoiseFloor": "Piso de Ruído Elevado", "excessiveFlooding": "Inundação excessiva", - "reticulumTrafficDetected": "Tráfego Reticulum detectado" + "reticulumTrafficDetected": "Tráfego Reticulum detectado", + "highCompanionTxQueue": "Fila High Companion TX" }, "rfCause": { "channelUtilizationSpike": "O {{current}}% atual está acima de 2vezes a média recente ({{average}}%) — possível aumento de congestionamento ou interferência.", @@ -1386,7 +1389,8 @@ "widebandNoiseFloor": "Interferência de banda larga (eletrônica defeituosa, ruído de linha de energia, etc.) elevando o nível de ruído.", "fringeWeakCoverage": "O nó está muito longe ou mal conectado ao resto da malha.", "elevatedNoiseFloor": "O nível de ruído de rádio é {{noiseFloor}} dBm — interferência elevada reduzindo o alcance efetivo.", - "excessiveFlooding": "{{percent}}% das transmissões são roteadas por inundação — o roteamento direto pode não ser estabelecido com nós próximos." + "excessiveFlooding": "{{percent}}% das transmissões são roteadas por inundação — o roteamento direto pode não ser estabelecido com nós próximos.", + "highCompanionTxQueue": "A profundidade da fila de saída do acompanhante é {{queueLen}}/256 — o tráfego pode estar atrasado ou cair." }, "foreignLoraCause": { "meshtastic": "Nó Meshtastic transmitindo nesta frequência. Remetente: {{sender}}. {{proximity}}Este nó pode estar na sua área de repetidor MeshCore.", @@ -1461,6 +1465,10 @@ "excessiveFlooding": { "title": "Permitir que rotas diretas sejam estabelecidas", "description": "A taxa de inundação é muito alta — aguarde o nó acumular contatos de caminho direto ou verifique a proximidade do repetidor." + }, + "highCompanionTxQueue": { + "title": "Reduza o tráfego de saída ou verifique o link de RF", + "description": "A fila do Companion TX está quase cheia — pause os envios, reduza as inundações ou verifique se há interferência e links fracos." } }, "cuHistoryHeading": "Utilização do Canal (últimas 24h)", @@ -1539,7 +1547,10 @@ "transportSaturatedShareInstance": "RNS transport saturated ({{count}} path-request drops). Share instance is on — quit other Reticulum apps (MeshChatX, Ratspeak, rnsd) and restart the stack.", "slowTransportQuery": "RNS transport queries slow or failing ({{count}}×)", "bleBondRemoved": "A ligação BLE RNode \"{{name}}\" está obsoleta — esqueça o dispositivo nas configurações de Bluetooth do sistema operacional e emparelhe novamente", - "blePairingTimedOut": "BLE RNode \"{{name}}\" a troca de senha expirou - insira o PIN atual (não 123456) e emparelhe novamente" + "blePairingTimedOut": "BLE RNode \"{{name}}\" a troca de senha expirou - insira o PIN atual (não 123456) e emparelhe novamente", + "sidecarUnhealthy": "O sidecar Reticulum está em execução, mas não responde às verificações de integridade", + "propagationSyncStuck": "A sincronização do nó de propagação está travada, estabelecendo um link", + "propagationSyncFailing": "Falha na sincronização do nó de propagação" } }, "routingPort": { @@ -3953,7 +3964,11 @@ "discoveredHash": "{{hash}}", "syncEstablishNoLinkProof": "A sincronização de propagação não obteve nenhuma prova de link do nó — sua identidade pode precisar de um novo anúncio ou o caminho de volta está faltando. Experimente Network → Announce agora e, em seguida, sincronize novamente.", "syncOfferUnknown": "O nó de propagação retornou um erro de oferta de sincronização desconhecido — tente novamente em um momento.", - "syncPathHint": "A sincronização remota precisa de um caminho nos dois sentidos. A sincronização envia um breve anúncio LXMF primeiro para que o nó possa responder." + "syncPathHint": "A sincronização remota precisa de um caminho nos dois sentidos. A sincronização envia um breve anúncio LXMF primeiro para que o nó possa responder.", + "setPreferredFailed": "Não foi possível definir o nó de propagação preferido.", + "localInboxName": "Propagação local (caixa de entrada offline)", + "localInboxHint": "A propagação local é a caixa de entrada offline deste dispositivo. Ele não armazena DMs de saída para pares inacessíveis — defina um nó lxmf.propagation remoto como preferido para armazenamento e encaminhamento.", + "preferredLocalWarning": "O nó preferido é apenas Local — os pares offline ainda precisam de um nó de propagação remota para entrega de armazenamento e encaminhamento." }, "reticulumPropagationHeader": { "modeLabel": "Modo de propagação", diff --git a/src/renderer/locales/ru/translation.json b/src/renderer/locales/ru/translation.json index 48e80d36a..8f1ab0e07 100644 --- a/src/renderer/locales/ru/translation.json +++ b/src/renderer/locales/ru/translation.json @@ -218,7 +218,6 @@ "clearReticulumMessagesConfirmButton": "Очистить {{count}} сообщения", "reticulumSection": "Reticulum — Стек ретикулума", "reticulumAnnounceHelp": "Как часто ваша личность объявляется в сети, и инструменты для очистки сохраненных объявлений.", - "reticulumPropagationHelp": "Выберите способ синхронизации узлов распространения и при необходимости извлеките последние данные распространения.", "logPanelHelp": "Если этот параметр включен, справа отображается поток журнала в реальном времени. На вкладке Reticulum строки устройства включают вывод дополнительных данных и состояние локального интерфейса. Строки отладки требуют наличия флажка внутри панели журнала.", "autoPruneUnheardContactsDaysAria": "Автоматическое удаление непрослушанных контактов при запуске, старше {{days}} дн.", "capTotalContactsCountAria": "Ограничить общее количество контактов, сохранить последние просмотренные контакты {{count}}", @@ -364,7 +363,8 @@ "shareLocationSendWaypointHint": "Если эта функция включена, Meshtastic отправляет как текстовое сообщение, так и пакет путевых точек, поэтому официальные приложения показывают отметку на карте. Отключите на занятых сетках, чтобы отправлять только текст.", "capStoredRrcMessages": "Ограничить сохранённую историю комнат RRC, хранить новейшие", "capStoredRrcMessagesLabel": "Ограничить сохранённую историю комнат RRC, хранить новейшие", - "capStoredRrcMessagesCountAria": "Ограничить сохраненную историю комнат RRC, сохранять новейшие сообщения {{count}}" + "capStoredRrcMessagesCountAria": "Ограничить сохраненную историю комнат RRC, сохранять новейшие сообщения {{count}}", + "reticulumPropagationHelp": "Настройте удаленные узлы распространения LXMF для автономных DM. Локальное распространение - это только почтовый ящик этого устройства — он не заменяет удаленный узел." }, "aria": { "closeDialog": "Закрыть диалог", @@ -613,7 +613,9 @@ "dmNodeHops_many": "{{count}} переходов", "dmNodeHops_other": "{{count}} перехода", "notificationDmTitle": "ДМ от {{sender}}", - "notificationMessageTitle": "Сообщение от {{sender}}" + "notificationMessageTitle": "Сообщение от {{sender}}", + "reticulumSendStoredAtPn": "Сохранено в узле распространения", + "reticulumPnAbbrev": "КН" }, "chatPayload": { "mention": "Упоминание {{label}}", @@ -1374,7 +1376,8 @@ "potentialMeshcoreRepeaterConflict": "Потенциальный конфликт повторителей MeshCore", "elevatedNoiseFloor": "Пол с повышенным уровнем шума", "excessiveFlooding": "Чрезмерное затопление", - "reticulumTrafficDetected": "Обнаружен трафик Reticulum" + "reticulumTrafficDetected": "Обнаружен трафик Reticulum", + "highCompanionTxQueue": "Очередь High Companion TX" }, "rfCause": { "channelUtilizationSpike": "Текущее значение {{current}}% более чем в 2 раза превышает недавнее среднее значение ({{average}}%) — возможна перегрузка или всплеск помех.", @@ -1388,7 +1391,8 @@ "widebandNoiseFloor": "Широкополосные помехи (неисправная электроника, шум в линии электропередачи и т. д.), повышающие минимальный уровень шума.", "fringeWeakCoverage": "Узел находится слишком далеко или плохо связан с остальной частью сетки.", "elevatedNoiseFloor": "Минимальный уровень радиошума составляет {{noiseFloor}} дБм — повышенные помехи, снижающие эффективную дальность.", - "excessiveFlooding": "{{percent}}% передач перенаправлены - прямая маршрутизация может быть не установлена с близлежащими узлами." + "excessiveFlooding": "{{percent}}% передач перенаправлены - прямая маршрутизация может быть не установлена с близлежащими узлами.", + "highCompanionTxQueue": "Глубина исходящей очереди спутника составляет {{queueLen}}/256 — трафик может быть задержан или пропущен." }, "foreignLoraCause": { "meshtastic": "Meshtastic узел передает на этой частоте. Отправитель: {{sender}}. {{proximity}}Этот узел может находиться в зоне повторителя MeshCore.", @@ -1463,6 +1467,10 @@ "excessiveFlooding": { "title": "Разрешить установку прямых маршрутов", "description": "Коэффициент затопления очень высок — подождите, пока узел накапливает прямые контакты, или проверьте близость ретранслятора." + }, + "highCompanionTxQueue": { + "title": "Уменьшите исходящий трафик или проверьте радиочастотную ссылку", + "description": "Очередь Companion TX почти заполнена — пауза отправляет, уменьшает наводнения или проверяет наличие помех и слабых связей." } }, "cuHistoryHeading": "Использование канала (последние 24 часа)", @@ -1541,7 +1549,10 @@ "transportSaturatedShareInstance": "RNS транспорт насыщенный ({{count}} path-request drops). Экземпляр Share включен — закройте другие приложения Reticulum (MeshChatX, Ratspeak, rnsd) и перезапустите стек.", "slowTransportQuery": "Транспортные запросы RNS медленные или сбойные ({{count}}×)", "bleBondRemoved": "Связь BLE RNode «{{name}}» устарела — забудьте в настройках Bluetooth ОС и выполните повторное сопряжение.", - "blePairingTimedOut": "Тайм-аут обмена ключами BLE RNode «{{name}}» — введите текущий PIN-код (не 123456) и повторите сопряжение." + "blePairingTimedOut": "Тайм-аут обмена ключами BLE RNode «{{name}}» — введите текущий PIN-код (не 123456) и повторите сопряжение.", + "sidecarUnhealthy": "Sidecar Reticulum работает, но не отвечает на проверки состояния", + "propagationSyncStuck": "Синхронизация узла распространения застряла при установлении канала", + "propagationSyncFailing": "Ошибка синхронизации узла распространения" } }, "routingPort": { @@ -3955,7 +3966,11 @@ "discoveredHash": "{{hash}}", "syncEstablishNoLinkProof": "Синхронизация распространения не получила подтверждения связи с узлом — вашей личности может потребоваться новое объявление или отсутствует обратный путь. Попробуйте Network → Announce сейчас, а затем синхронизировать снова.", "syncOfferUnknown": "Узел распространения вернул неизвестную ошибку предложения синхронизации — повторите попытку через мгновение.", - "syncPathHint": "Удаленной синхронизации нужен путь в обе стороны. Синхронизация сначала отправляет короткое объявление LXMF, чтобы узел мог ответить." + "syncPathHint": "Удаленной синхронизации нужен путь в обе стороны. Синхронизация сначала отправляет короткое объявление LXMF, чтобы узел мог ответить.", + "setPreferredFailed": "Не удалось установить предпочтительный узел распространения.", + "localInboxName": "Локальное распространение (автономный почтовый ящик)", + "localInboxHint": "Локальное распространение - это автономный почтовый ящик этого устройства. Он не хранит исходящие DM для недоступных одноранговых узлов — установите удаленный узел lxmf.propagation в качестве предпочтительного для хранения и пересылки.", + "preferredLocalWarning": "Предпочтительным узлом является только локальный — автономные одноранговые узлы по-прежнему нуждаются в удаленном узле распространения для доставки в хранилище." }, "reticulumPropagationHeader": { "modeLabel": "Режим распространения", diff --git a/src/renderer/locales/tr/translation.json b/src/renderer/locales/tr/translation.json index ceeb4dfdf..2ac5b7d14 100644 --- a/src/renderer/locales/tr/translation.json +++ b/src/renderer/locales/tr/translation.json @@ -218,7 +218,6 @@ "clearReticulumMessagesConfirmButton": "{{count}} mesajı temizle", "reticulumSection": "Reticulum — Reticulum yığını", "reticulumAnnounceHelp": "Kimliğinizin ağda ne sıklıkta duyurulduğu ve saklanan duyuruları temizleme araçları.", - "reticulumPropagationHelp": "Yayılma düğümlerinin nasıl senkronize edileceğini seçin ve gerektiğinde en son yayılma verilerini çekin.", "logPanelHelp": "Etkinleştirildiğinde sağda canlı bir günlük akışı görünür. Reticulum sekmesinde, cihaz hatları sepet çıkışını ve yerel arayüz durumunu içerir. Hata ayıklama satırları, günlük panelinin içindeki onay kutusunun işaretlenmesini gerektirir.", "autoPruneUnheardContactsDaysAria": "Başlangıçta {{days}} günden eski, duyulmamış kişileri otomatik olarak buda", "capTotalContactsCountAria": "Toplam kişileri sınırlandırın, en son görülen {{count}} kişileri koruyun", @@ -364,7 +363,8 @@ "shareLocationSendWaypointHint": "Etkinleştirildiğinde, Meshtastic konum paylaşımları hem kısa mesaj hem de Yol Noktası paketi göndererek resmi uygulamaların bir harita işaretçisi göstermesini sağlar. Yalnızca metin göndermek için meşgul ağlarda devre dışı bırakın.", "capStoredRrcMessages": "Saklanan RRC oda geçmişini sınırlayın, en yeniyi koruyun", "capStoredRrcMessagesLabel": "Saklanan RRC oda geçmişini sınırlayın, en yeniyi koruyun", - "capStoredRrcMessagesCountAria": "Saklanan RRC oda geçmişini sınırlayın, en yeni {{count}} mesajlarını saklayın" + "capStoredRrcMessagesCountAria": "Saklanan RRC oda geçmişini sınırlayın, en yeni {{count}} mesajlarını saklayın", + "reticulumPropagationHelp": "Çevrimdışı DM'ler için uzak LXMF yayılım düğümlerini yapılandırın. Yerel yayılım yalnızca bu cihazın gelen kutusudur — uzak bir düğümün yerini almaz." }, "aria": { "closeDialog": "İletişim kutusunu kapat", @@ -611,7 +611,9 @@ "dmNodeHops_one": "{{count}} atlama", "dmNodeHops_other": "{{count}} atlama", "notificationDmTitle": "{{sender}} 'dan DM", - "notificationMessageTitle": "{{sender}} adlı kişiden mesaj" + "notificationMessageTitle": "{{sender}} adlı kişiden mesaj", + "reticulumSendStoredAtPn": "Yayılma düğümünde depolanır", + "reticulumPnAbbrev": "PN" }, "chatPayload": { "mention": "{{label}}'dan bahsedin", @@ -1372,7 +1374,8 @@ "potentialMeshcoreRepeaterConflict": "Potansiyel MeshCore Tekrarlayıcı Çatışması", "elevatedNoiseFloor": "Yükseltilmiş Gürültü Zemini", "excessiveFlooding": "Aşırı Su Baskını", - "reticulumTrafficDetected": "Reticulum Trafiği Algılandı" + "reticulumTrafficDetected": "Reticulum Trafiği Algılandı", + "highCompanionTxQueue": "Yüksek Refakatçi TX Kuyruğu" }, "rfCause": { "channelUtilizationSpike": "Mevcut {{current}}%, yakın zamandaki ortalamanın ({{average}}%) 2 katının üzerinde; olası tıkanıklık veya parazit dalgalanması.", @@ -1386,7 +1389,8 @@ "widebandNoiseFloor": "Gürültü tabanını yükselten geniş bant paraziti (arızalı elektronikler, elektrik hattı gürültüsü vb.).", "fringeWeakCoverage": "Düğüm çok uzakta veya ağın geri kalanına zayıf bir şekilde bağlı.", "elevatedNoiseFloor": "Radyo gürültüsü zemini {{noiseFloor}} dBm'dir — etkili menzili azaltan yüksek parazit.", - "excessiveFlooding": "İletimlerin % {{percent}}kadarı taşkın yönlendirmelidir — yakındaki düğümlerle doğrudan yönlendirme kurulamayabilir." + "excessiveFlooding": "İletimlerin % {{percent}}kadarı taşkın yönlendirmelidir — yakındaki düğümlerle doğrudan yönlendirme kurulamayabilir.", + "highCompanionTxQueue": "Refakatçi giden kuyruk derinliği {{queueLen}}/256 'dır — trafik gecikebilir veya düşebilir." }, "foreignLoraCause": { "meshtastic": "Meshtastic düğüm bu frekansta iletim yapıyor. Gönderen: {{sender}}. {{proximity}}Bu düğüm MeshCore tekrarlayıcı alanınızda olabilir.", @@ -1461,6 +1465,10 @@ "excessiveFlooding": { "title": "Doğrudan rotaların oluşturulmasına izin ver", "description": "Taşkın oranı çok yüksektir — düğümün doğrudan yol temasları biriktirmesini bekleyin veya tekrarlayıcı yakınlığını kontrol edin." + }, + "highCompanionTxQueue": { + "title": "Giden trafiği azaltın veya RF bağlantısını kontrol edin", + "description": "Refakatçi TX kuyruğu neredeyse dolu — gönderimleri duraklatın, taşkınları azaltın veya parazit ve zayıf bağlantı olup olmadığını kontrol edin." } }, "cuHistoryHeading": "Kanal Kullanımı (son 24 saat)", @@ -1539,7 +1547,10 @@ "transportSaturatedShareInstance": "RNS nakli doymuş ({{count}} yol talebi düşer). Paylaşım örneği açık — diğer Reticulum uygulamalarından (MeshChatX, Ratspeak, rnsd) çıkın ve yığını yeniden başlatın.", "slowTransportQuery": "RNS taşıma sorguları yavaş veya başarısız ({{count}}×)", "bleBondRemoved": "BLE RNode \"{{name}}\" bağı eski — İşletim Sistemi Bluetooth ayarlarını unutun ve yeniden eşleştirin", - "blePairingTimedOut": "BLE RNode \"{{name}}\" şifre değişimi zaman aşımına uğradı — mevcut PIN'i girin (123456 değil) ve yeniden eşleştirin" + "blePairingTimedOut": "BLE RNode \"{{name}}\" şifre değişimi zaman aşımına uğradı — mevcut PIN'i girin (123456 değil) ve yeniden eşleştirin", + "sidecarUnhealthy": "Reticulum sepeti çalışıyor ancak sağlık kontrollerine yanıt vermiyor", + "propagationSyncStuck": "Yayılım düğümü senkronizasyonu bir bağlantı kurarken takıldı", + "propagationSyncFailing": "Yayılma düğümü senkronizasyonu başarısız oldu" } }, "routingPort": { @@ -3953,7 +3964,11 @@ "discoveredHash": "{{hash}}", "syncEstablishNoLinkProof": "Yayılma senkronizasyonunun düğümden bağlantı kanıtı yok. Kimliğinizin yeni bir duyuruya ihtiyacı olabilir veya geri dönüş yolu eksik olabilir. Şimdi Network → Announce'ı deneyin, ardından tekrar Senkronize edin.", "syncOfferUnknown": "Yayılma düğümü bilinmeyen bir senkronizasyon teklifi hatası verdi — birazdan tekrar deneyin.", - "syncPathHint": "Uzaktan senkronizasyonun her iki yönde de bir yola ihtiyacı vardır. SYNC, düğümün yanıt verebilmesi için önce kısa bir LXMF duyurusu gönderir." + "syncPathHint": "Uzaktan senkronizasyonun her iki yönde de bir yola ihtiyacı vardır. SYNC, düğümün yanıt verebilmesi için önce kısa bir LXMF duyurusu gönderir.", + "setPreferredFailed": "Tercih edilen yayılma düğümü ayarlanamadı.", + "localInboxName": "Yerel yayılım (çevrimdışı gelen kutusu)", + "localInboxHint": "Yerel yayılım, bu cihazın çevrimdışı gelen kutusudur. Ulaşılamayan eşler için giden DM'leri saklamaz — depolama ve iletme için tercih edilen bir uzak lxmf.propagation düğümü ayarlayın.", + "preferredLocalWarning": "Tercih edilen düğüm yalnızca Yereldir — çevrimdışı eşler, depolama ve iletme teslimatı için hala uzak bir yayılma düğümüne ihtiyaç duyarlar." }, "reticulumPropagationHeader": { "modeLabel": "Yayılma modu", diff --git a/src/renderer/locales/uk/translation.json b/src/renderer/locales/uk/translation.json index 9feebddf5..5809c1791 100644 --- a/src/renderer/locales/uk/translation.json +++ b/src/renderer/locales/uk/translation.json @@ -218,7 +218,6 @@ "clearReticulumMessagesConfirmButton": "Очистити повідомлення {{count}}", "reticulumSection": "Reticulum — Стек ретикулуму", "reticulumAnnounceHelp": "Як часто ваша особа оголошується в мережі, а також інструменти для очищення збережених оголошень.", - "reticulumPropagationHelp": "Виберіть спосіб синхронізації вузлів розповсюдження та витягуйте найновіші дані розповсюдження, коли це необхідно.", "logPanelHelp": "Якщо ввімкнено, праворуч з’являється пряма трансляція журналу. На вкладці Reticulum рядки пристроїв включають вихідні дані та стан локального інтерфейсу. Рядки налагодження потребують прапорця на панелі журналу.", "autoPruneUnheardContactsDaysAria": "Автоматично видаляти непрослухані контакти під час запуску, старші {{days}} днів", "capTotalContactsCountAria": "Обмежте загальну кількість контактів, зберігайте останні {{count}} контактів", @@ -364,7 +363,8 @@ "shareLocationSendWaypointHint": "Коли ввімкнено, Meshtastic надсилає як текстове повідомлення, так і пакет Waypoint, щоб офіційні програми показували маркер на карті. Вимкніть на зайнятих сітках, щоб надсилати лише текст.", "capStoredRrcMessages": "Обмежити збережену історію кімнат RRC, зберігати найновіші", "capStoredRrcMessagesLabel": "Обмежити збережену історію кімнат RRC, зберігати найновіші", - "capStoredRrcMessagesCountAria": "Обмежити збережену історію кімнат RRC, зберігати найновіші {{count}} повідомлення" + "capStoredRrcMessagesCountAria": "Обмежити збережену історію кімнат RRC, зберігати найновіші {{count}} повідомлення", + "reticulumPropagationHelp": "Налаштуйте віддалені вузли розповсюдження LXMF для автономних DM. Локальне поширення - це лише папка «Вхідні» цього пристрою — вона не замінює віддалений вузол." }, "aria": { "closeDialog": "Закрити діалог", @@ -613,7 +613,9 @@ "dmNodeHops_many": "{{count}} переходів", "dmNodeHops_other": "{{count}} переходу", "notificationDmTitle": "ДМ від {{sender}}", - "notificationMessageTitle": "Повідомлення від {{sender}}" + "notificationMessageTitle": "Повідомлення від {{sender}}", + "reticulumSendStoredAtPn": "Зберігається на вузлі розповсюдження", + "reticulumPnAbbrev": "PN" }, "chatPayload": { "mention": "Згадайте {{label}}", @@ -1374,7 +1376,8 @@ "potentialMeshcoreRepeaterConflict": "Потенційний конфлікт ретранслятора MeshCore", "elevatedNoiseFloor": "Підвищена шумова підлога", "excessiveFlooding": "Надмірна повінь", - "reticulumTrafficDetected": "Виявлено трафік Reticulum" + "reticulumTrafficDetected": "Виявлено трафік Reticulum", + "highCompanionTxQueue": "Висока черга супутника TX" }, "rfCause": { "channelUtilizationSpike": "Поточний {{current}}% перевищує 2рази останній середній показник ({{average}}%) — можливі затори або перешкоди.", @@ -1388,7 +1391,8 @@ "widebandNoiseFloor": "Широкосмугові перешкоди (несправна електроніка, шум лінії електропередач тощо), що піднімають підлогу шуму.", "fringeWeakCoverage": "Вузол знаходиться занадто далеко або погано з'єднаний з рештою сіті.", "elevatedNoiseFloor": "Рівень радіошуму становить {{noiseFloor}} дБм — підвищені перешкоди, що зменшують ефективну дальність.", - "excessiveFlooding": "{{percent}}% передач передаються по затопленому маршруту — пряма маршрутизація може бути не встановлена з сусідніми вузлами." + "excessiveFlooding": "{{percent}}% передач передаються по затопленому маршруту — пряма маршрутизація може бути не встановлена з сусідніми вузлами.", + "highCompanionTxQueue": "Супутня вихідна черга має глибину {{queueLen}}/256 — трафік може затримуватися або пропускатися." }, "foreignLoraCause": { "meshtastic": "Meshtastic вузол, що передає на цій частоті. Відправник: {{sender}}. {{proximity}}Цей вузол може знаходитися в області повторювача MeshCore.", @@ -1463,6 +1467,10 @@ "excessiveFlooding": { "title": "Дозволити встановлювати прямі маршрути", "description": "Коефіцієнт затоплення дуже високий — зачекайте, поки вузол накопичить контакти прямого шляху, або перевірте близькість повторювача." + }, + "highCompanionTxQueue": { + "title": "Зменште вихідний трафік або перевірте радіочастотне посилання", + "description": "Супутня черга TX майже заповнена — призупиніть надсилання, зменште повені або перевірте наявність перешкод і слабких зв'язків." } }, "cuHistoryHeading": "Використання каналу (останні 24 години)", @@ -1541,7 +1549,10 @@ "transportSaturatedShareInstance": "Транспорт RNS насичений ({{count}} краплі запиту шляху). Екземпляр Share увімкнено — закрийте інші програми Reticulum (MeshChatX, Ratspeak, rnsd) і перезапустіть стек.", "slowTransportQuery": "Транспортні запити RNS повільні або невдалі ({{count}}×)", "bleBondRemoved": "Зв’язок BLE RNode \"{{name}}\" застарілий — видаліть пристрій у налаштуваннях Bluetooth ОС і повторіть сполучення", - "blePairingTimedOut": "Час очікування обміну ключем доступу BLE RNode \"{{name}}\" минув — введіть поточний PIN-код (не 123456) і повторіть сполучення" + "blePairingTimedOut": "Час очікування обміну ключем доступу BLE RNode \"{{name}}\" минув — введіть поточний PIN-код (не 123456) і повторіть сполучення", + "sidecarUnhealthy": "Sidecar Reticulum працює, але не відповідає на перевірки здоров'я", + "propagationSyncStuck": "Синхронізація вузла розповсюдження застрягла під час встановлення зв'язку", + "propagationSyncFailing": "Помилка синхронізації вузла поширення" } }, "routingPort": { @@ -3955,7 +3966,11 @@ "discoveredHash": "{{hash}}", "syncEstablishNoLinkProof": "Синхронізація поширення не отримала підтвердження посилання від вузла — вашій ідентичності може знадобитися нове оголошення, або зворотний шлях відсутній. Спробуйте Network → Announce зараз, а потім синхронізуйте знову.", "syncOfferUnknown": "Вузол поширення повернув невідому помилку пропозиції синхронізації — повторіть спробу через мить.", - "syncPathHint": "Для віддаленої синхронізації потрібен шлях в обидві сторони. Синхронізація надсилає спочатку коротке оголошення LXMF, щоб вузол міг відповісти." + "syncPathHint": "Для віддаленої синхронізації потрібен шлях в обидві сторони. Синхронізація надсилає спочатку коротке оголошення LXMF, щоб вузол міг відповісти.", + "setPreferredFailed": "Не вдалося встановити бажаний вузол розповсюдження.", + "localInboxName": "Локальне поширення (офлайн-вхідні)", + "localInboxHint": "Локальне поширення - це офлайн-папка вхідних цього пристрою. Він не зберігає вихідні DM для недоступних вузлів — встановіть віддалений вузол lxmf.propagation як бажаний для зберігання та пересилання.", + "preferredLocalWarning": "Бажаний вузол є лише локальним — автономним вузлам все ще потрібен віддалений вузол розповсюдження для зберігання та пересилання." }, "reticulumPropagationHeader": { "modeLabel": "Режим поширення", diff --git a/src/renderer/locales/zh/translation.json b/src/renderer/locales/zh/translation.json index b4420b77e..56997af8e 100644 --- a/src/renderer/locales/zh/translation.json +++ b/src/renderer/locales/zh/translation.json @@ -218,7 +218,6 @@ "clearReticulumMessagesConfirmButton": "清除{{count}}消息", "reticulumSection": "Reticulum — 网状堆栈", "reticulumAnnounceHelp": "您的身份在网络上公布的频率,以及清除存储的公告的工具。", - "reticulumPropagationHelp": "选择传播节点的同步方式,并在需要时提取最新传播数据。", "logPanelHelp": "启用后,实时日志流将显示在右侧。在 Reticulum 选项卡上,设备行包括 sidecar 输出和本地接口运行状况。调试行需要日志面板内的复选框。", "autoPruneUnheardContactsDaysAria": "启动时自动删除未听过的、超过 {{days}} 天的联系人", "capTotalContactsCountAria": "联系人总数上限,保留最近查看的 {{count}} 联系人", @@ -364,7 +363,8 @@ "shareLocationSendWaypointHint": "启用后,Meshtastic 位置共享会发送短信和航点数据包,以便官方应用程序显示地图图钉。在繁忙的网格上禁用以仅发送文本。", "capStoredRrcMessages": "存储RRC房间历史上限,保持最新", "capStoredRrcMessagesLabel": "存储RRC房间历史上限,保持最新", - "capStoredRrcMessagesCountAria": "限制存储的 RRC 房间历史记录,保留最新的 {{count}} 消息" + "capStoredRrcMessagesCountAria": "限制存储的 RRC 房间历史记录,保留最新的 {{count}} 消息", + "reticulumPropagationHelp": "为离线DM配置远程LXMF传播节点。本地传播仅是此设备的收件箱—它不会替换远程节点。" }, "aria": { "closeDialog": "关闭对话框", @@ -611,7 +611,9 @@ "dmNodeHops_one": "{{count}}跳", "dmNodeHops_other": "{{count}}跳", "notificationDmTitle": "来自{{sender}}的DM", - "notificationMessageTitle": "来自{{sender}}的消息" + "notificationMessageTitle": "来自{{sender}}的消息", + "reticulumSendStoredAtPn": "存储在传播节点", + "reticulumPnAbbrev": "PN" }, "chatPayload": { "mention": "提及{{label}}", @@ -1372,7 +1374,8 @@ "potentialMeshcoreRepeaterConflict": "潜在的MeshCore中继器冲突", "elevatedNoiseFloor": "高架噪音地板", "excessiveFlooding": "洪水泛滥", - "reticulumTrafficDetected": "检测到 Reticulum 流量" + "reticulumTrafficDetected": "检测到 Reticulum 流量", + "highCompanionTxQueue": "高伴侣TX队列" }, "rfCause": { "channelUtilizationSpike": "当前的{{current}} %是近期平均值( {{average}} % )的2倍以上—可能是拥堵或干扰浪涌。", @@ -1386,7 +1389,8 @@ "widebandNoiseFloor": "宽带干扰(电子设备故障、电源线噪声等)导致噪声底板升高。", "fringeWeakCoverage": "节点距离太远或与网格其余部分的连接不佳。", "elevatedNoiseFloor": "无线电噪声底板为{{noiseFloor}} dBm — 干扰升高,有效范围缩短。", - "excessiveFlooding": "{{percent}}%的传输是洪泛路由 — 可能未与附近节点建立直接路由。" + "excessiveFlooding": "{{percent}}%的传输是洪泛路由 — 可能未与附近节点建立直接路由。", + "highCompanionTxQueue": "伴侣出站队列深度为{{queueLen}}/256 —流量可能会延迟或丢弃。" }, "foreignLoraCause": { "meshtastic": "在此频率上传输的Meshtastic节点。发件人: {{sender}}. {{proximity}}此节点可能位于您的MeshCore中继器区域。", @@ -1461,6 +1465,10 @@ "excessiveFlooding": { "title": "允许建立直接路由", "description": "洪水比非常高—等待节点积累直接路径触点,或检查中继器接近度。" + }, + "highCompanionTxQueue": { + "title": "减少出站流量或检查射频链路", + "description": "伴侣TX队列几乎已满—暂停发送、减少洪水或检查干扰和薄弱环节。" } }, "cuHistoryHeading": "渠道利用率(过去24小时)", @@ -1539,7 +1547,10 @@ "transportSaturatedShareInstance": "RNS传输饱和( {{count}}路径请求丢弃)。共享实例已打开—退出其他Reticulum应用程序( MeshChatX、Ratspeak、rnsd )并重新启动堆栈。", "slowTransportQuery": "RNS传输查询缓慢或失败({{count}} ×)", "bleBondRemoved": "BLE RNode「{{name}}」绑定已过时 — 在操作系统蓝牙设置中忽略该设备并重新配对", - "blePairingTimedOut": "BLE RNode“{{name}}”密钥交换超时 — 输入当前 PIN(不是 123456)并重新配对" + "blePairingTimedOut": "BLE RNode“{{name}}”密钥交换超时 — 输入当前 PIN(不是 123456)并重新配对", + "sidecarUnhealthy": "Reticulum sidecar正在运行,但未响应健康检查", + "propagationSyncStuck": "传播节点同步卡住,无法建立链接", + "propagationSyncFailing": "传播节点同步失败" } }, "routingPort": { @@ -3953,7 +3964,11 @@ "discoveredHash": "{{hash}}", "syncEstablishNoLinkProof": "传播同步没有从节点获得链接证明—您的身份可能需要新的公告,或者缺少返回路径。立即尝试网络→公告,然后再次同步。", "syncOfferUnknown": "传播节点返回了一个未知的同步报价错误—请稍后重试。", - "syncPathHint": "远程同步需要双向路径。Sync首先发送一个简短的LXMF通告,以便节点可以回复。" + "syncPathHint": "远程同步需要双向路径。Sync首先发送一个简短的LXMF通告,以便节点可以回复。", + "setPreferredFailed": "无法设置首选传播节点。", + "localInboxName": "本地传播(离线收件箱)", + "localInboxHint": "本地传播是此设备的离线收件箱。它不存储无法访问的对等项的出站DM —将远程lxmf.propagation节点设置为存储和转发的首选节点。", + "preferredLocalWarning": "首选节点仅限本地—离线对等节点仍然需要远程传播节点进行存储和转发交付。" }, "reticulumPropagationHeader": { "modeLabel": "传播模式", diff --git a/src/renderer/runtime/useReticulumRuntime.reconnect-hardening.test.ts b/src/renderer/runtime/useReticulumRuntime.reconnect-hardening.test.ts index e15a7034f..a3996b919 100644 --- a/src/renderer/runtime/useReticulumRuntime.reconnect-hardening.test.ts +++ b/src/renderer/runtime/useReticulumRuntime.reconnect-hardening.test.ts @@ -210,7 +210,7 @@ describe('useReticulumRuntime contact → nodeStore label preservation', () => { describe('useReticulumRuntime outbound delivery persistence', () => { it('persists Completes/Fails via applyReticulumOutboundDeliveryStatus', () => { expect(SOURCE).toMatch( - /evt\.type === 'lxmf_outbound_status'[\s\S]*?applyReticulumOutboundDeliveryStatus\(identityId, p\.message_hash, p\.status,\s*\{\s*sentVia: p\.sent_via,\s*\}\)/, + /evt\.type === 'lxmf_outbound_status'[\s\S]*?applyReticulumOutboundDeliveryStatus\(identityId, p\.message_hash, p\.status,\s*\{\s*sentVia: p\.sent_via,\s*deliveryMethod: p\.delivery_method,\s*\}\)/, ); }); @@ -218,6 +218,14 @@ describe('useReticulumRuntime outbound delivery persistence', () => { expect(SOURCE).toMatch(/flushPendingReticulumOutboundDeliveryStatus\(identityId, hash\)/); }); + it('wires propagation store + sidecar health into Reticulum diagnostics', () => { + expect(SOURCE).toMatch(/sidecarUnhealthySince:\s*sidecarStatus\.unhealthySince/); + expect(SOURCE).toMatch(/useReticulumPropagationStore\.subscribe/); + expect(SOURCE).toMatch(/RETICULUM_PROPAGATION_SYNC_STALL_MS \+ 1_000/); + expect(SOURCE).toMatch(/status\.healthy === false/); + expect(SOURCE).toMatch(/activePropagationSyncAttemptAt/); + }); + it('marks stale outbound with RETICULUM_STALE_OUTBOUND_MS (not a 5-minute override)', () => { expect(SOURCE).toContain('RETICULUM_STALE_OUTBOUND_MS'); expect(SOURCE).toMatch( diff --git a/src/renderer/runtime/useReticulumRuntime.ts b/src/renderer/runtime/useReticulumRuntime.ts index 9230ceb04..8d8a63fc1 100644 --- a/src/renderer/runtime/useReticulumRuntime.ts +++ b/src/renderer/runtime/useReticulumRuntime.ts @@ -52,7 +52,10 @@ import { setReticulumManualStackStopSuppress, } from '@/renderer/lib/reticulum/reticulumManualStackStopSuppress'; import { failReticulumSendingOutboundToDestHash } from '@/renderer/lib/reticulum/reticulumOutboundFailureBridge'; -import { applyPropagationSyncEvent } from '@/renderer/lib/reticulum/reticulumPropagationSync'; +import { + applyPropagationSyncEvent, + RETICULUM_PROPAGATION_SYNC_STALL_MS, +} from '@/renderer/lib/reticulum/reticulumPropagationSync'; import { reticulumWireRowToEntry } from '@/renderer/lib/reticulum/reticulumRawPacketLog'; import { resolveReticulumSelfFullLabel, @@ -386,6 +389,7 @@ export function useReticulumRuntime(): ProtocolRuntime { const selfNodeId = selfLxmfHash ? reticulumHashToNodeId(selfLxmfHash) : 0; const shareInstanceEnabled = stackRaw != null ? parseReticulumStackSettingsPayload(stackRaw).share_instance : false; + const propState = useReticulumPropagationStore.getState(); const rows = buildReticulumDiagnosticRows(snapshot, { selfNodeId, interfaces, @@ -394,6 +398,16 @@ export function useReticulumRuntime(): ProtocolRuntime { autoBeaconAlert: sidecarStatus.autoBeaconAlert ?? null, interfaceIssueAlert: sidecarStatus.interfaceIssueAlert ?? null, shareInstanceEnabled, + sidecarRunning: sidecarStatus.running, + sidecarHealthy: sidecarStatus.healthy, + sidecarUnhealthySince: sidecarStatus.unhealthySince, + propagation: { + syncActive: propState.sync.active, + syncProgress: propState.sync.progress, + lastSyncError: propState.lastSyncError, + lastAttemptAt: + propState.activePropagationSyncAttemptAt ?? propState.lastPropagationSyncAttemptAt, + }, }); useDiagnosticsStore.setState((s) => ({ diagnosticRows: mergeReticulumDiagnosticRows(s.diagnosticRows, rows), @@ -440,6 +454,43 @@ export function useReticulumRuntime(): ProtocolRuntime { }, 2_000); }, [syncDiagnosticsFromSidecar]); + // Refresh diagnostics when propagation sync starts/ends outside WS (startSync fail, stall cancel). + useEffect(() => { + let prevActive = useReticulumPropagationStore.getState().sync.active; + let prevError = useReticulumPropagationStore.getState().lastSyncError; + let stallRefreshTimer: ReturnType | null = null; + const clearStallRefresh = () => { + if (stallRefreshTimer) { + clearTimeout(stallRefreshTimer); + stallRefreshTimer = null; + } + }; + const unsub = useReticulumPropagationStore.subscribe((state) => { + const activeChanged = state.sync.active !== prevActive; + const errorChanged = state.lastSyncError !== prevError; + if (state.sync.active && !prevActive) { + clearStallRefresh(); + // Catch stuck/failing shortly after the establishing stall window. + stallRefreshTimer = setTimeout(() => { + stallRefreshTimer = null; + scheduleDebouncedDiagnosticsRefresh(); + }, RETICULUM_PROPAGATION_SYNC_STALL_MS + 1_000); + } + if (!state.sync.active) { + clearStallRefresh(); + } + prevActive = state.sync.active; + prevError = state.lastSyncError; + if (activeChanged || errorChanged) { + scheduleDebouncedDiagnosticsRefresh(); + } + }); + return () => { + clearStallRefresh(); + unsub(); + }; + }, [scheduleDebouncedDiagnosticsRefresh]); + const appendRawPacket = useCallback((entry: ReticulumRawPacketEntry) => { rawPacketAppenderRef.current?.append(entry); useReticulumPacketStore.getState().appendPacket(entry); @@ -550,10 +601,12 @@ export function useReticulumRuntime(): ProtocolRuntime { message_hash?: string; status?: string; sent_via?: string; + delivery_method?: string; }; if (identityId && p.message_hash && p.status) { applyReticulumOutboundDeliveryStatus(identityId, p.message_hash, p.status, { sentVia: p.sent_via, + deliveryMethod: p.delivery_method, }); } } @@ -564,6 +617,7 @@ export function useReticulumRuntime(): ProtocolRuntime { ) { const p = evt.payload as { progress?: number; active?: boolean; message?: string | null }; applyPropagationSyncEvent(p); + scheduleDebouncedDiagnosticsRefresh(); } if (evt.type === 'propagation.discovered' && evt.payload && typeof evt.payload === 'object') { const p = evt.payload as { @@ -1021,7 +1075,7 @@ export function useReticulumRuntime(): ProtocolRuntime { useEffect(() => { const unsubStatus = window.electronAPI.reticulum.onStatus((status) => { - if (status.interfaceIssueAlert || status.autoBeaconAlert) { + if (status.interfaceIssueAlert || status.autoBeaconAlert || status.healthy === false) { void syncDiagnosticsFromSidecar(); const timeouts = status.interfaceIssueAlert?.linkDeliveryTimeouts; if (identityId && timeouts?.length) { diff --git a/src/renderer/stores/messageStore.ts b/src/renderer/stores/messageStore.ts index 72f2274b4..642393bf2 100644 --- a/src/renderer/stores/messageStore.ts +++ b/src/renderer/stores/messageStore.ts @@ -1,5 +1,7 @@ import { create } from 'zustand'; +import type { ReticulumDeliveryMethod } from '@/shared/reticulumDeliveryMethod'; + import type { IdentityId } from '../lib/types'; import { omitRecordKey } from './storeUtils'; @@ -48,8 +50,8 @@ export interface MessageRecord { reticulumSenderHash?: string; /** Reticulum reply target message hash (hex). */ reticulumReplyToHash?: string; - /** Reticulum LXMF delivery method when queued (direct / propagated / opportunistic). */ - reticulumDeliveryMethod?: 'direct' | 'propagated' | 'opportunistic'; + /** Reticulum LXMF delivery method when queued (direct / propagated / opportunistic / paper). */ + reticulumDeliveryMethod?: ReticulumDeliveryMethod; /** Saved attachment path on disk (local saves). */ reticulumAttachmentPath?: string; /** Message was replayed from a Store & Forward server (Meshtastic only). */ @@ -91,6 +93,7 @@ const MESSAGE_RECORD_KEYS: (keyof MessageRecord)[] = [ 'reticulumMessageHash', 'reticulumSenderHash', 'reticulumReplyToHash', + 'reticulumDeliveryMethod', 'reticulumAttachmentPath', 'viaStoreForward', ]; diff --git a/src/renderer/stores/reticulumPropagationStore.ts b/src/renderer/stores/reticulumPropagationStore.ts index f9013ea26..d186ced98 100644 --- a/src/renderer/stores/reticulumPropagationStore.ts +++ b/src/renderer/stores/reticulumPropagationStore.ts @@ -13,6 +13,9 @@ import { } from '@/renderer/lib/reticulum/reticulumSidecarReads'; import { RETICULUM_PROPAGATION_AUTO_SYNC_DEFAULT_SEC } from '@/shared/reticulumPropagationAutoSync'; +/** i18n key written when the user cancels an in-flight propagation sync. */ +export const PROPAGATION_SYNC_USER_CANCEL_KEY = 'reticulumPropagation.syncCancelled'; + export interface PropagationNodeRow { id: string; name: string; @@ -281,11 +284,11 @@ export const useReticulumPropagationStore = create { - const fallback = opts?.reasonKey ?? 'reticulumPropagation.syncCancelled'; + const fallback = opts?.reasonKey ?? PROPAGATION_SYNC_USER_CANCEL_KEY; const existing = state.lastSyncError; const keepSidecar = existing != null && - existing !== 'reticulumPropagation.syncCancelled' && + existing !== PROPAGATION_SYNC_USER_CANCEL_KEY && existing !== 'reticulumPropagation.syncTimedOut'; return { sync: { ...RETICULUM_PROPAGATION_SYNC_IDLE }, @@ -298,11 +301,11 @@ export const useReticulumPropagationStore = create { - const fallback = opts?.reasonKey ?? 'reticulumPropagation.syncCancelled'; + const fallback = opts?.reasonKey ?? PROPAGATION_SYNC_USER_CANCEL_KEY; const existing = state.lastSyncError; const keepSidecar = existing != null && - existing !== 'reticulumPropagation.syncCancelled' && + existing !== PROPAGATION_SYNC_USER_CANCEL_KEY && existing !== 'reticulumPropagation.syncTimedOut'; return { sync: { ...RETICULUM_PROPAGATION_SYNC_IDLE }, diff --git a/src/shared/electron-api.types.ts b/src/shared/electron-api.types.ts index 9753c57ce..558950979 100644 --- a/src/shared/electron-api.types.ts +++ b/src/shared/electron-api.types.ts @@ -351,6 +351,7 @@ export interface ElectronAPI { message_hash?: string | null; received_via?: string | null; delivery_status?: string | null; + delivery_method?: string | null; delivery_attempts?: number | null; next_delivery_attempt_at?: number | null; attachment_path?: string | null; diff --git a/src/shared/reticulumDeliveryMethod.test.ts b/src/shared/reticulumDeliveryMethod.test.ts new file mode 100644 index 000000000..57766fd95 --- /dev/null +++ b/src/shared/reticulumDeliveryMethod.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest'; + +import { parseReticulumDeliveryMethod } from '@/shared/reticulumDeliveryMethod'; + +describe('parseReticulumDeliveryMethod', () => { + it('accepts known methods case-insensitively', () => { + expect(parseReticulumDeliveryMethod('direct')).toBe('direct'); + expect(parseReticulumDeliveryMethod('Propagated')).toBe('propagated'); + expect(parseReticulumDeliveryMethod('opportunistic')).toBe('opportunistic'); + expect(parseReticulumDeliveryMethod('paper')).toBe('paper'); + }); + + it('rejects unknown or empty values', () => { + expect(parseReticulumDeliveryMethod(undefined)).toBeUndefined(); + expect(parseReticulumDeliveryMethod(null)).toBeUndefined(); + expect(parseReticulumDeliveryMethod('')).toBeUndefined(); + expect(parseReticulumDeliveryMethod('garbage')).toBeUndefined(); + }); +}); diff --git a/src/shared/reticulumDeliveryMethod.ts b/src/shared/reticulumDeliveryMethod.ts new file mode 100644 index 000000000..cf9da1562 --- /dev/null +++ b/src/shared/reticulumDeliveryMethod.ts @@ -0,0 +1,20 @@ +/** Wire / SQLite LXMF delivery method labels (sidecar `delivery_method_label`). */ +export const RETICULUM_DELIVERY_METHODS = [ + 'direct', + 'propagated', + 'opportunistic', + 'paper', +] as const; + +export type ReticulumDeliveryMethod = (typeof RETICULUM_DELIVERY_METHODS)[number]; + +const ALLOWED = new Set(RETICULUM_DELIVERY_METHODS); + +/** Parse sidecar / DB delivery_method; unknown values → undefined. */ +export function parseReticulumDeliveryMethod( + value: string | undefined | null, +): ReticulumDeliveryMethod | undefined { + if (value == null || value === '') return undefined; + const normalized = value.trim().toLowerCase(); + return ALLOWED.has(normalized) ? (normalized as ReticulumDeliveryMethod) : undefined; +}