diff --git a/docs/agents/reticulum.md b/docs/agents/reticulum.md index cd7ae28bb..c9c54484a 100644 --- a/docs/agents/reticulum.md +++ b/docs/agents/reticulum.md @@ -8,7 +8,7 @@ Deep subsystem reference for AI assistants. Open this when a task touches the Re - **Deep links / QR:** OS scheme is **`lxm://`** (not `mesh-client://`); `MeshClientDeepLinkHost`, `meshClientDeepLink.ts` (`lxmPaperMessage` kind + `looksLikeLxmPaperBlob`; Games `lxm://game/` / Ratspeak `lrgp:` → `lxmGameSession`), `handleReticulumQrIngest.ts` (shared Network/Chat/OS paper + in-app contact ingest), `applyLxmPaperIngest` → `POST /api/v1/lxmf/paper/ingest`, `QrIngestControl` / `QrCodeImage`. OS contact / MeshCore imports confirm before upsert; **paper OS deep links ingest without confirm**; Games session links open Reticulum Games tab via `openReticulumGameSession`. - **Decommissioned hubs:** `src/shared/reticulumDecommissionedHubs.ts` (Amsterdam only) — stack-start auto-disable + **Add default backbones** disables matching enabled TCP rows; UI badge + enable-block in `ReticulumInterfacesPanel.tsx` (`isDecommissionedReticulumTcpInterfaceRow`); keep TS↔Rust synced via `pnpm run check:reticulum-decommissioned-hubs`. Default backbone picker + region-grouped interface list (Primary & Global / North America / Europe / Asia & Oceania / Specialty / User Defined) in `reticulumDefaultHubPresets.ts` + `ReticulumDefaultHubsPickerModal.tsx`; muted disabled rows + checkbox bulk delete; `countEnabledDefaultHubPresets` / >3 enable warning - **BLE RNode RSSI:** `useReticulumBleRnodeRssiMap` gates on sidecar **running** (not api-ready), burst-then-steady scans via nested `acquireReticulumBleScan`, clears sticky targets immediately when all BLE RNodes are disabled -- **Propagation mode / sync:** Network → Propagation nodes owns Off/Auto/Manual (default **Off**; persisted values including legacy App-panel `auto` are honored). Auto one-time syncs the best Discovered PN by destination hash (no Add, no Preferred write) via `startPropagationSyncCascade` + sidecar `destination_hash` sync, then configured remotes, then local-prop (skips remotes when no enabled interfaces); runtime hook `useReticulumPropagationAutoSync`. Manual uses Preferred, else picks the best configured remote **for that sync only** (no Preferred write), then the remaining remotes, then local-prop. Off = **no PN support**: `startPropagationSyncCascade` returns early (per-row Sync is disabled in UI), `hasEffectiveReticulumPropagationTarget` / `hasReticulumPnCascadeCapacity` are false, `ReticulumPropagationNotice` is hidden, and the sidecar disarms the outbound PN plus empties cascade candidates (`propagation_mode` in `mesh_client_stack.json`, `POST /api/v1/propagation/mode`, `candidates_for_propagation_mode`); renderer pushes the mode on change and on sidecar-ready. `reticulumPropagationStore` / `reticulumPropagationSync.ts` — Complete on HaveAll, Establishing stall (~45s) + hard ceiling (~180s), auto-sync interval from last success with failure cooldown, error keys for identity / non-PN / peering stamp; stamps `lastPropagationSyncAttemptAt` / `activePropagationSyncAttemptAt` for WS correlation. **Nothing-to-sync is not a failure:** when the cascade contacts no node it writes `syncNoTarget` / `syncLocalLoading` (never overwriting a real error from an attempted node), the local row reports sidecar `status: "loading"` while the messagestore reads (`local_propagation_status` + `PropagationBridge::messagestore_load_pending`, per-row Sync disabled), and the 30 s tick calls `refreshFromSidecar` while `hasPropagationCascadeCandidate` is false so a fresh stack recovers on its own — `refreshFromSidecar` must **not** clear the active attempt while `sync.active`. Debug snapshot `propagationClient` exposes mode/preferred/autoTarget/resolvedSyncTargetId. **Auto also deposits on Discovered PNs:** sidecar `auto_discovered_candidates` (`pn_cascade.rs`, Auto only, cap 3, hop-sorted, skips inactive / self / already-configured / over `max_peering_cost`) appends after configured remotes and before local-prop, rebuilt from the shared `rebuild_pn_cascade_candidates` helper in `live.rs` (called by `refresh_pn_cascade_candidates` **and** the PN announce handler); `hasEffectiveReticulumPropagationTarget` / `hasReticulumPnCascadeCapacity` therefore count discovered rows in Auto, so the Chat notice hides and the link-timeout failure bridge holds off. **Chat notice dismiss:** `chatNoticeDismissed` (`mesh-client:reticulumPropagationNoticeDismissed`) with **Don't show again** on the banner and **Show propagation reminder in Chat** in the Network section. **Named sync target:** `startSync` stamps `syncTargetId`; progress line, inline error, and Sync toasts resolve it with `resolveReticulumPropagationTargetLabel`; the cascade clears it when nothing was contacted so `syncNoTarget` / `syncLocalLoading` stay unprefixed. **Attempts settle before the cascade advances:** `startSync` returns `accepted` | `deferred` | `failed` (not a boolean) — only sidecar _acceptance_ starts `awaitPropagationSyncSettled` (terminal WS frame or stall/ceiling watchdog). `failed` advances with ~15 min session-memory omit via `reticulumPropagationSyncBackoff.ts`; `deferred` (`PROPAGATION_SYNC_OUTBOUND_BUSY` — outbound deposit owns the PN link) advances **without** backoff so the next tick may retry; `cancelled` (user Cancel) stops; `success` ends the run. Remote steps are capped by `PROPAGATION_CASCADE_BUDGET_MS` (5 min) then fall through to local-prop; each remote attempt is capped by `PROPAGATION_CASCADE_ATTEMPT_TIMEOUT_MS` (~60s); local fallback refreshes nodes when local looks disabled; the cascade is single-flight (`resetPropagationSyncCascadeState` is the test seam) so overlapping 30 s ticks join one run while an explicit per-row Sync supersedes it. Auto `/api/v1/interfaces` probe **fails open** (assumes interfaces enabled) so a broken proxy still tries remotes before local. **Retrieval vs peer sync:** the WS progress bar / HaveAll reflects the `/offer` peer sync (`PropagationSyncTask`, inventory replication only). The half that actually pulls your mail into Chat is the **client `/get` download** (`PropagationClient`), wired in `propagation_download.rs` + `propagation_bridge.rs::poll_client_download` and driven by `live.rs::spawn_client_download_driver` (list → get → purge; decrypt with local identity → router delivery callback). `local-prop` Sync uses `PropagationBridge::drain_local_inbox` (in-process node `/get` replay). Logs: real inbox retrieval is `propagation-retrieve` (`retrieve_mode=get|local`, `listed`/`downloaded`/`delivered`); the peer-offer outcome is `propagation-sync … peer_outcome=have_all|transfer` (**not** retrieval). +- **Propagation mode / sync:** Network → Propagation nodes owns Off/Auto/Manual (default **Off**; persisted values including legacy App-panel `auto` are honored). Auto one-time syncs via `startPropagationSyncCascade` + sidecar `destination_hash` sync in order: **finite-hop discovered** (no Add/Preferred) → **configured remotes** → **unknown-hop discovered** → local-prop (skips remotes when no enabled interfaces); runtime hook `useReticulumPropagationAutoSync`. Sidecar `start_propagation_sync` hard-fails with `PROPAGATION_PATH_UNKNOWN` when `ensure_path_for_direct` fails after announce settle (same code as offer probe — no Establishing stall). Manual uses Preferred, else picks the best configured remote **for that sync only** (no Preferred write), then the remaining remotes, then local-prop. Off = **no PN support**: `startPropagationSyncCascade` returns early (per-row Sync is disabled in UI), `hasEffectiveReticulumPropagationTarget` / `hasReticulumPnCascadeCapacity` are false, `ReticulumPropagationNotice` is hidden, and the sidecar disarms the outbound PN plus empties cascade candidates (`propagation_mode` in `mesh_client_stack.json`, `POST /api/v1/propagation/mode`, `candidates_for_propagation_mode`); renderer pushes the mode on change and on sidecar-ready. `reticulumPropagationStore` / `reticulumPropagationSync.ts` — Complete on HaveAll, Establishing stall (~45s) + hard ceiling (~180s), auto-sync interval from last success with failure cooldown, error keys for identity / non-PN / peering stamp; stamps `lastPropagationSyncAttemptAt` / `activePropagationSyncAttemptAt` for WS correlation. **Nothing-to-sync is not a failure:** when the cascade contacts no node it writes `syncNoTarget` / `syncLocalLoading` (never overwriting a real error from an attempted node), the local row reports sidecar `status: "loading"` while the messagestore reads (`local_propagation_status` + `PropagationBridge::messagestore_load_pending`, per-row Sync disabled), and the 30 s tick calls `refreshFromSidecar` while `hasPropagationCascadeCandidate` is false so a fresh stack recovers on its own — `refreshFromSidecar` must **not** clear the active attempt while `sync.active`. Debug snapshot `propagationClient` exposes mode/preferred/autoTarget/resolvedSyncTargetId. **Auto also deposits on Discovered PNs:** sidecar `auto_discovered_candidates` (`pn_cascade.rs`, Auto only, cap 3, hop-sorted, skips inactive / self / already-configured / over `max_peering_cost`) appends after configured remotes and before local-prop, rebuilt from the shared `rebuild_pn_cascade_candidates` helper in `live.rs` (called by `refresh_pn_cascade_candidates` **and** the PN announce handler); `hasEffectiveReticulumPropagationTarget` / `hasReticulumPnCascadeCapacity` therefore count discovered rows in Auto, so the Chat notice hides and the link-timeout failure bridge holds off. **Chat notice dismiss:** `chatNoticeDismissed` (`mesh-client:reticulumPropagationNoticeDismissed`) with **Don't show again** on the banner and **Show propagation reminder in Chat** in the Network section. **Named sync target:** `startSync` stamps `syncTargetId`; progress line, inline error, and Sync toasts resolve it with `resolveReticulumPropagationTargetLabel`; the cascade clears it when nothing was contacted so `syncNoTarget` / `syncLocalLoading` stay unprefixed. **Attempts settle before the cascade advances:** `startSync` returns `accepted` | `deferred` | `failed` (not a boolean) — only sidecar _acceptance_ starts `awaitPropagationSyncSettled` (terminal WS frame or stall/ceiling watchdog). `failed` advances with ~15 min session-memory omit via `reticulumPropagationSyncBackoff.ts`; `deferred` (`PROPAGATION_SYNC_OUTBOUND_BUSY` — outbound deposit owns the PN link) advances **without** backoff so the next tick may retry; `cancelled` (user Cancel) stops; `success` ends the run. Remote steps are capped by `PROPAGATION_CASCADE_BUDGET_MS` (5 min) then fall through to local-prop; each remote attempt is capped by `PROPAGATION_CASCADE_ATTEMPT_TIMEOUT_MS` (~60s); local fallback refreshes nodes when local looks disabled; the cascade is single-flight (`resetPropagationSyncCascadeState` is the test seam) so overlapping 30 s ticks join one run while an explicit per-row Sync supersedes it. Auto `/api/v1/interfaces` probe **fails open** (assumes interfaces enabled) so a broken proxy still tries remotes before local. **Retrieval vs peer sync:** the WS progress bar / HaveAll reflects the `/offer` peer sync (`PropagationSyncTask`, inventory replication only). The half that actually pulls your mail into Chat is the **client `/get` download** (`PropagationClient`), wired in `propagation_download.rs` + `propagation_bridge.rs::poll_client_download` and driven by `live.rs::spawn_client_download_driver` (list → get → purge; decrypt with local identity → router delivery callback). `local-prop` Sync uses `PropagationBridge::drain_local_inbox` (in-process node `/get` replay). Logs: real inbox retrieval is `propagation-retrieve` (`retrieve_mode=get|local`, `listed`/`downloaded`/`delivered`); the peer-offer outcome is `propagation-sync … peer_outcome=have_all|transfer` (**not** retrieval). - **PN hosting:** Network **Advanced PN hosting** / `ReticulumPnHostingDangerZone`; shared `pnHostingPolicy.ts` + sidecar `pn_hosting_policy.rs` / `pn_hosting_apply.rs`; `POST /api/v1/propagation/hosting-policy`; rsLXMF policy-setters overlay ([ratspeak/rsLXMF#6](https://github.com/ratspeak/rsLXMF/pull/6)). Messagestore loads in background on live attach; enabled `local-prop` serve/announce waits until load completes. - **Interface modes:** rnsd `mode` via `reticulumInterfaceMode.ts` + sidecar `normalize_interface_mode` (keep catalogs in sync — `pnpm run check:reticulum-interface-modes` in pre-commit/`release.sh`); add defaults TCP/UDP/I2P → `boundary`, RNode → `access_point`; UI in `ReticulumInterfacesPanel`; default hub presets add/repair missing mode to `boundary` (do not overwrite valid non-boundary). See [../reticulum.md#interface-modes](../reticulum.md#interface-modes). - **Share instance defaults:** missing keys bootstrap to `share_instance = No` / `instance_name = mesh-client` (does not overwrite explicit Yes/`default`); SharedInstanceClient banner + `disable_share_instance` repair; offline lint via `reticulum:validateConfig` / Network **Check config** / `pnpm run reticulum:config:check` diff --git a/reticulum-sidecar/src/stack/live.rs b/reticulum-sidecar/src/stack/live.rs index fae5b7a89..5dcb59ed0 100644 --- a/reticulum-sidecar/src/stack/live.rs +++ b/reticulum-sidecar/src/stack/live.rs @@ -3559,9 +3559,9 @@ impl LiveBridge { // Re-apply persisted PN pubkey before identity wait (announce flood may have evicted it). self.rehydrate_propagation_identities_from_persisted(); // Link proofs are ignored unless the destination pubkey is in known_identities. - // Resolve identity (+ path when possible) before Establishing, same as LXMF delivery. + // Resolve identity before Establishing; hard path gate comes after announce settle + // and before peering PoW (never start Establishing / stamp work without a path). let identity_ok = self.ensure_identity_for_direct(&dest_hex).await; - let _path_ok = self.ensure_path_for_direct(&dest_hex, true).await; let identity_known_after = self .outbound .lock() @@ -3581,15 +3581,6 @@ impl LiveBridge { } return Err("PROPAGATION_TARGET_NOT_PN".into()); } - let peering = match self.resolve_propagation_peering(&dest_hex).await { - Ok(p) => p, - Err(e) => { - if let Ok(mut driver) = self.outbound.lock() { - driver.set_propagation_sync_target(None); - } - return Err(e); - } - }; // Fresh LXMF delivery announce so the PN can return LRPROOF (reverse path). let announced = self .ensure_lxmf_announce_for_propagation_sync(&dest_hex) @@ -3620,7 +3611,33 @@ impl LiveBridge { ); return Err("PROPAGATION_SYNC_OUTBOUND_BUSY".into()); } + // Hard path gate after announce settle so RequestPath can benefit from the announce. + // Do this before peering PoW — nonzero peering_cost stamps are expensive and useless + // when there is no path (would previously burn CPU then stall into syncTimedOut). + let path_ok = self.ensure_path_for_direct(&dest_hex, true).await; let hops = self.hops_to_destination(&dest_hex).await; + if !path_ok { + if let Ok(mut driver) = self.outbound.lock() { + driver.set_propagation_sync_target(None); + } + tracing::info!( + target: "propagation-sync", + dest = %dest_hex, + path_ok, + hops = ?hops, + "propagation sync aborted — no path to PN" + ); + return Err("PROPAGATION_PATH_UNKNOWN".into()); + } + let peering = match self.resolve_propagation_peering(&dest_hex).await { + Ok(p) => p, + Err(e) => { + if let Ok(mut driver) = self.outbound.lock() { + driver.set_propagation_sync_target(None); + } + return Err(e); + } + }; // Pin PN pubkey for the duration of Establishing so announce-flood eviction // cannot drop it before LRPROOF validation (see known_identities cap). let pinned = if let Ok(mut driver) = self.outbound.lock() { @@ -3636,6 +3653,7 @@ impl LiveBridge { tracing::info!( target: "propagation-sync", dest = %dest_hex, + path_ok, hops = ?hops, pinned, "starting remote propagation sync" diff --git a/reticulum-sidecar/src/stack/propagation_bridge.rs b/reticulum-sidecar/src/stack/propagation_bridge.rs index 69d33438b..4eaf8cf11 100644 --- a/reticulum-sidecar/src/stack/propagation_bridge.rs +++ b/reticulum-sidecar/src/stack/propagation_bridge.rs @@ -1089,6 +1089,45 @@ mod tests { live.contains("spawn_client_download_driver") && live.contains("start_client_download"), "live sync must drive the client `/get` download" ); + // Remote sync must hard-fail when no path exists (same as offer probe) instead of + // starting Establishing and timing out in the renderer. + let sync_fn_start = live + .find("pub async fn start_propagation_sync") + .expect("start_propagation_sync"); + let sync_fn = &live[sync_fn_start..]; + let sync_fn_end = sync_fn[1..] + .find("\n pub ") + .map_or(sync_fn.len(), |idx| idx + 1); + let sync_body = &sync_fn[..sync_fn_end]; + assert!( + sync_body.contains("PROPAGATION_PATH_UNKNOWN"), + "start_propagation_sync must return PROPAGATION_PATH_UNKNOWN when path is missing" + ); + assert!( + !sync_body.contains("let _path_ok"), + "start_propagation_sync must not discard ensure_path_for_direct" + ); + let path_gate_at = sync_body + .find("PROPAGATION_PATH_UNKNOWN") + .expect("PATH_UNKNOWN in start_propagation_sync"); + let peering_at = sync_body + .find("resolve_propagation_peering") + .expect("peering resolve in start_propagation_sync"); + let start_sync_at = sync_body + .find("start_sync(hash") + .expect("start_sync call in start_propagation_sync"); + assert!( + path_gate_at < peering_at, + "path gate must run before peering PoW (nonzero peering_cost)" + ); + assert!( + peering_at < start_sync_at, + "peering must run after path succeeds and before start_sync" + ); + assert!( + path_gate_at < start_sync_at, + "path gate must run before start_sync / Establishing" + ); } /// Auto deposits on discovered PNs, so a newly heard announce must refresh the cascade diff --git a/src/main/support-bundle.test.ts b/src/main/support-bundle.test.ts index 8c2bdbdd2..094b7b136 100644 --- a/src/main/support-bundle.test.ts +++ b/src/main/support-bundle.test.ts @@ -109,6 +109,9 @@ describe('extractLxmfOutboundLogSlice', () => { 'debug peer refresh ok', 'info target=propagation-deposit outbound PN deposit Completes', 'info target=propagation-retrieve sync transfer progress', + 'info target=propagation-sync propagation sync aborted — no path to PN', + 'warn propagation establish failed: NoLinkProof', + 'error PROPAGATION_PATH_UNKNOWN', ].join('\n'), 'utf8', ); @@ -117,6 +120,9 @@ describe('extractLxmfOutboundLogSlice', () => { expect(slice).toContain('DeliverPropagated'); expect(slice).toContain('propagation-deposit'); expect(slice).toContain('propagation-retrieve'); + expect(slice).toContain('propagation-sync'); + expect(slice).toContain('propagation establish'); + expect(slice).toContain('PROPAGATION_PATH_UNKNOWN'); expect(slice).not.toContain('hello world'); expect(slice).not.toContain('peer refresh ok'); }); diff --git a/src/main/support-bundle.ts b/src/main/support-bundle.ts index d538d7331..021552ab7 100644 --- a/src/main/support-bundle.ts +++ b/src/main/support-bundle.ts @@ -189,6 +189,9 @@ export function extractLxmfOutboundLogSlice(...logChunks: Buffer[]): Buffer { /lxmf-outbound/i, /propagation-deposit/i, /propagation-retrieve/i, + /propagation-sync/i, + /propagation establish/i, + /PROPAGATION_PATH_UNKNOWN/i, /LXMF advancing PN cascade/i, /LXMF outbound delivery failed/i, /Direct path failover/i, diff --git a/src/renderer/lib/reticulum/reticulumPropagationAutoApply.test.ts b/src/renderer/lib/reticulum/reticulumPropagationAutoApply.test.ts index 43a0d476d..98df3e0f0 100644 --- a/src/renderer/lib/reticulum/reticulumPropagationAutoApply.test.ts +++ b/src/renderer/lib/reticulum/reticulumPropagationAutoApply.test.ts @@ -208,6 +208,37 @@ describe('reticulumPropagationAutoApply', () => { expect(startSync).toHaveBeenCalledWith('local-prop'); }); + it('Auto tries configured remotes before hops-unknown discovered announces', async () => { + const unknownHash = '2222'.repeat(8); + const startSync = vi + .fn() + .mockResolvedValueOnce('failed') + .mockResolvedValueOnce('failed') + .mockResolvedValueOnce('accepted'); + useReticulumPropagationStore.setState({ + preferredId: null, + nodes: [ + { id: 'local-prop', name: 'Local', enabled: true, status: 'known' }, + { + id: 'pn-aabb1111', + name: 'Remote', + enabled: true, + status: 'known', + hops: 2, + destination_hash: 'aabb'.repeat(8), + }, + ], + discovered: [{ destination_hash: unknownHash, node_state: true, peering_cost: 0 }], + startSync, + }); + await expect(startPropagationSyncCascade({ hasEnabledInterfaces: true })).resolves.toBe(true); + expect(startSync.mock.calls.map((c) => c[0])).toEqual([ + 'pn-aabb1111', + unknownHash, + 'local-prop', + ]); + }); + it('Manual Preferred local-prop syncs local settle', async () => { writeReticulumPropagationMode('manual'); const startSync = vi.fn().mockResolvedValue('accepted'); diff --git a/src/renderer/lib/reticulum/reticulumPropagationAutoApply.ts b/src/renderer/lib/reticulum/reticulumPropagationAutoApply.ts index 612c64290..399c420d2 100644 --- a/src/renderer/lib/reticulum/reticulumPropagationAutoApply.ts +++ b/src/renderer/lib/reticulum/reticulumPropagationAutoApply.ts @@ -2,7 +2,8 @@ import { hasEnabledLocalPropagationNode, isLocalPropagationLoading, listConfiguredRemotePropagationIds, - listDiscoveredPropagationTargets, + listFiniteHopDiscoveredPropagationTargets, + listUnknownHopDiscoveredPropagationTargets, propagationTargetDestinationHash, readReticulumPropagationMode, resolveManualCascadeSeed, @@ -180,8 +181,8 @@ async function runConfiguredRemoteAttempts(args: { } /** - * Auto: best discovered (one-time sync by hash — **no** Add, **no** Preferred) → - * configured remotes → local-prop settle. + * Auto: finite-hop discovered (one-time by hash — **no** Add/Preferred) → configured + * remotes → unknown-hop discovered → local-prop settle. * Manual: explicit first target, else Preferred, else best configured remote (picked for this * sync only — **no** Preferred write) → remaining configured remotes → local-prop settle. * Off: no propagation support — never syncs, even with an explicit target. @@ -190,7 +191,7 @@ async function runConfiguredRemoteAttempts(args: { * then fails to establish hands off to the next candidate instead of ending the cascade. * * In Auto, `firstTargetId` is ignored — per-row Sync and bottom Sync both run the full - * discovered → configured → local cascade. + * finite-discovered → configured → unknown-discovered → local cascade. */ export async function startPropagationSyncCascade(opts?: { /** Seeds Manual (Preferred / per-row Sync). Ignored in Auto. */ @@ -240,20 +241,33 @@ async function runPropagationSyncCascade( } const tried = new Set(); - const discoveredTargets = omitRecentlyFailedPropagationTargets( - listDiscoveredPropagationTargets(nodes, discovered), - (target) => target.destinationHash, - ).slice(0, MAX_DISCOVERED_SYNC_ATTEMPTS); + const tryDiscoveredBatch = async ( + batch: { destinationHash: string; hops: number }[], + ): Promise<'success' | 'cancelled' | 'continue'> => { + const targets = omitRecentlyFailedPropagationTargets( + batch, + (target) => target.destinationHash, + ).slice(0, MAX_DISCOVERED_SYNC_ATTEMPTS); + for (const target of targets) { + if (superseded('auto')) return 'cancelled'; + if (remoteBudgetSpent()) return 'continue'; + const hash = target.destinationHash.toLowerCase(); + if (tried.has(hash)) continue; + tried.add(hash); + const outcome = await attemptSync(hash, attempts); + if (outcome === 'success') return 'success'; + if (outcome === 'cancelled') return 'cancelled'; + } + return 'continue'; + }; - for (const target of discoveredTargets) { - if (superseded('auto')) return false; - if (remoteBudgetSpent()) break; - const hash = target.destinationHash.toLowerCase(); - tried.add(hash); - const outcome = await attemptSync(hash, attempts); - if (outcome === 'success') return true; - if (outcome === 'cancelled') return false; - } + // Prefer path-known discovered PNs before configured remotes; leave hops-unknown + // vanity announces until after configured so they cannot starve Preferred/added PNs. + const finiteOutcome = await tryDiscoveredBatch( + listFiniteHopDiscoveredPropagationTargets(nodes, discovered), + ); + if (finiteOutcome === 'success') return true; + if (finiteOutcome === 'cancelled') return false; const remotes = await runConfiguredRemoteAttempts({ mode: 'auto', @@ -264,6 +278,13 @@ async function runPropagationSyncCascade( }); if (remotes === 'success') return true; if (remotes === 'stop') return false; + + const unknownOutcome = await tryDiscoveredBatch( + listUnknownHopDiscoveredPropagationTargets(nodes, discovered), + ); + if (unknownOutcome === 'success') return true; + if (unknownOutcome === 'cancelled') return false; + return tryLocalSettleIfEnabled(attempts); } @@ -295,7 +316,8 @@ async function runPropagationSyncCascade( /** * Run the mode-appropriate sync cascade with an optional Manual seed target. - * Auto ignores `targetId` and always runs discovered → configured → local. + * Auto ignores `targetId` and always runs finite-discovered → configured → + * unknown-discovered → local. */ export async function startPropagationSyncWithTarget(targetId: string): Promise { return startPropagationSyncCascade({ firstTargetId: targetId }); diff --git a/src/renderer/lib/reticulum/reticulumPropagationMode.test.ts b/src/renderer/lib/reticulum/reticulumPropagationMode.test.ts index 5ab3e642f..38559755b 100644 --- a/src/renderer/lib/reticulum/reticulumPropagationMode.test.ts +++ b/src/renderer/lib/reticulum/reticulumPropagationMode.test.ts @@ -191,6 +191,16 @@ describe('reticulumPropagationMode', () => { }); }); + it('prefers configured remotes over hops-unknown discovered announces', () => { + const nodes = [row({ id: 'pn-aaaa', name: 'Added', hops: 4 })]; + const rows = [discovered({ destination_hash: '2222'.repeat(8) })]; + expect(pickAutoPropagationTarget(nodes, rows)).toEqual({ + kind: 'configured', + id: 'pn-aaaa', + }); + expect(resolvePropagationSyncTargetId('auto', nodes, null, rows)).toBe('pn-aaaa'); + }); + it('ignores discovered rows already configured or inactive', () => { const hash = 'aabb'.repeat(8); const nodes = [row({ id: 'pn-aabb', name: 'Configured', hops: 2, destination_hash: hash })]; diff --git a/src/renderer/lib/reticulum/reticulumPropagationMode.ts b/src/renderer/lib/reticulum/reticulumPropagationMode.ts index e0491b6e9..71ebe666b 100644 --- a/src/renderer/lib/reticulum/reticulumPropagationMode.ts +++ b/src/renderer/lib/reticulum/reticulumPropagationMode.ts @@ -71,11 +71,21 @@ function sortByHopsThenKey(a: T, b: T): number { return a.sortKey.localeCompare(b.sortKey); } +export interface DiscoveredPropagationTarget { + destinationHash: string; + hops: number; +} + +/** True when hops came from the path table (not “unknown” / Infinity). */ +export function hasFinitePropagationHops(hops: number): boolean { + return Number.isFinite(hops); +} + /** Active discovered remotes not already configured, best (lowest hops) first. */ export function listDiscoveredPropagationTargets( nodes: PropagationNodeRow[], discovered: readonly DiscoveredPropagationRow[], -): { destinationHash: string; hops: number }[] { +): DiscoveredPropagationTarget[] { const configuredHashes = configuredPropagationDestinationHashes(nodes); const rows: { destinationHash: string; hops: number; sortKey: string }[] = []; for (const row of discovered) { @@ -92,6 +102,26 @@ export function listDiscoveredPropagationTargets( return rows.map(({ destinationHash, hops }) => ({ destinationHash, hops })); } +/** Discovered PNs with a known hop count (path table), best first. */ +export function listFiniteHopDiscoveredPropagationTargets( + nodes: PropagationNodeRow[], + discovered: readonly DiscoveredPropagationRow[], +): DiscoveredPropagationTarget[] { + return listDiscoveredPropagationTargets(nodes, discovered).filter((t) => + hasFinitePropagationHops(t.hops), + ); +} + +/** Discovered PNs with unknown hops (announce heard, no path yet), name/hash sorted. */ +export function listUnknownHopDiscoveredPropagationTargets( + nodes: PropagationNodeRow[], + discovered: readonly DiscoveredPropagationRow[], +): DiscoveredPropagationTarget[] { + return listDiscoveredPropagationTargets(nodes, discovered).filter( + (t) => !hasFinitePropagationHops(t.hops), + ); +} + /** Enabled configured remotes (excludes local-prop), best (lowest hops) first. */ export function listConfiguredRemotePropagationIds(nodes: PropagationNodeRow[]): string[] { const rows: { id: string; hops: number; sortKey: string }[] = []; @@ -181,9 +211,10 @@ export function pickAutoPropagationTarget( nodes: PropagationNodeRow[], discovered: readonly DiscoveredPropagationRow[] = [], ): AutoPropagationTarget | null { - const discoveredBest = listDiscoveredPropagationTargets(nodes, discovered).at(0); - if (discoveredBest != null) { - return { kind: 'discovered', destinationHash: discoveredBest.destinationHash }; + // Finite-hop discovered → configured → unknown-hop discovered → local (matches cascade). + const finiteBest = listFiniteHopDiscoveredPropagationTargets(nodes, discovered).at(0); + if (finiteBest != null) { + return { kind: 'discovered', destinationHash: finiteBest.destinationHash }; } const configuredBest = listConfiguredRemotePropagationIds(nodes).at(0); @@ -191,6 +222,11 @@ export function pickAutoPropagationTarget( return { kind: 'configured', id: configuredBest }; } + const unknownBest = listUnknownHopDiscoveredPropagationTargets(nodes, discovered).at(0); + if (unknownBest != null) { + return { kind: 'discovered', destinationHash: unknownBest.destinationHash }; + } + if (hasReadyEnabledLocalPropagationNode(nodes)) { return { kind: 'local' }; } @@ -209,9 +245,9 @@ export function pickAutoPropagationNodeId(nodes: PropagationNodeRow[]): string | /** * Sync target hint for UI enablement. * - * Auto: best discovered destination hash (one-time sync), else best configured remote, - * else local-prop. Manual uses Preferred (including `local-prop`), else picks the best - * configured remote for this sync only (no Preferred write), else local-prop. Off → null. + * Auto: finite-hop discovered → configured remote → unknown-hop discovered → local-prop. + * Manual uses Preferred (including `local-prop`), else picks the best configured remote + * for this sync only (no Preferred write), else local-prop. Off → null. */ export function resolvePropagationSyncTargetId( mode: ReticulumPropagationMode, @@ -226,13 +262,10 @@ export function resolvePropagationSyncTargetId( if (configuredBest != null) return configuredBest; return hasReadyEnabledLocalPropagationNode(nodes) ? 'local-prop' : null; } - const discoveredBest = listDiscoveredPropagationTargets(nodes, discovered).at(0); - if (discoveredBest != null) { - return discoveredBest.destinationHash.toLowerCase(); - } - const configured = listConfiguredRemotePropagationIds(nodes).at(0); - if (configured != null) return configured; - if (hasReadyEnabledLocalPropagationNode(nodes)) return 'local-prop'; + const target = pickAutoPropagationTarget(nodes, discovered); + if (target?.kind === 'discovered') return target.destinationHash.toLowerCase(); + if (target?.kind === 'configured') return target.id; + if (target?.kind === 'local') return 'local-prop'; return null; } diff --git a/src/renderer/lib/reticulum/reticulumPropagationSync.test.ts b/src/renderer/lib/reticulum/reticulumPropagationSync.test.ts index 742307cbc..804ed0228 100644 --- a/src/renderer/lib/reticulum/reticulumPropagationSync.test.ts +++ b/src/renderer/lib/reticulum/reticulumPropagationSync.test.ts @@ -89,6 +89,9 @@ describe('reticulumPropagationSync', () => { expect(mapPropagationSyncError('PROPAGATION_IDENTITY_UNKNOWN')).toBe( 'reticulumPropagation.syncIdentityUnknown', ); + expect(mapPropagationSyncError('PROPAGATION_PATH_UNKNOWN')).toBe( + 'reticulumPropagation.syncPathUnknown', + ); expect(mapPropagationSyncError('PROPAGATION_TARGET_NOT_PN')).toBe( 'reticulumPropagation.syncTargetNotPropagationNode', ); diff --git a/src/renderer/lib/reticulum/reticulumPropagationSync.ts b/src/renderer/lib/reticulum/reticulumPropagationSync.ts index 1898d1715..6211d975c 100644 --- a/src/renderer/lib/reticulum/reticulumPropagationSync.ts +++ b/src/renderer/lib/reticulum/reticulumPropagationSync.ts @@ -56,6 +56,7 @@ export function isPropagationSyncCancelledMessage(message: string | null | undef } const SYNC_IDENTITY_UNKNOWN_KEY = 'reticulumPropagation.syncIdentityUnknown'; const SYNC_TARGET_NOT_PN_KEY = 'reticulumPropagation.syncTargetNotPropagationNode'; +const SYNC_PATH_UNKNOWN_KEY = 'reticulumPropagation.syncPathUnknown'; const SYNC_PEERAGE_STAMP_FAILED_KEY = 'reticulumPropagation.syncPeeringStampFailed'; const SYNC_ESTABLISH_IDENTITY_KEY = 'reticulumPropagation.syncEstablishIdentityMissing'; const SYNC_ESTABLISH_INVALID_KEY = 'reticulumPropagation.syncEstablishInvalidProof'; @@ -140,6 +141,7 @@ export function mapPropagationSyncError(error: string | null | undefined): strin const byPrefix = mapPropagationSyncErrorByPrefix(error); if (byPrefix) return byPrefix; if (error === 'PROPAGATION_TARGET_NOT_PN') return SYNC_TARGET_NOT_PN_KEY; + if (error === 'PROPAGATION_PATH_UNKNOWN') return SYNC_PATH_UNKNOWN_KEY; if (error === 'PROPAGATION_PEER_COST_EXCEEDS_MAX') return SYNC_PEER_COST_EXCEEDS_MAX_KEY; if (error === 'PROPAGATION_OFFER_UNSUPPORTED') return SYNC_OFFER_UNSUPPORTED_KEY; if (error === 'PROPAGATION_OFFER_PROBE_TIMEOUT') return SYNC_OFFER_PROBE_TIMEOUT_KEY; diff --git a/src/renderer/locales/cs/translation.json b/src/renderer/locales/cs/translation.json index 8c24b724a..778976b97 100644 --- a/src/renderer/locales/cs/translation.json +++ b/src/renderer/locales/cs/translation.json @@ -4291,7 +4291,8 @@ "syncErrorWithTarget": "{{name}}: {{message}}", "showChatNotice": "Zobrazit připomenutí propagace v chatu", "showChatNoticeAria": "Zobrazit banner připomenutí propagačního uzlu v chatu", - "showChatNoticeHint": "Vypnutím tohoto tlačítka skryjete banner chatu, který se zobrazí, když není k dispozici žádný propagační uzel." + "showChatNoticeHint": "Vypnutím tohoto tlačítka skryjete banner chatu, který se zobrazí, když není k dispozici žádný propagační uzel.", + "syncPathUnknown": "Zatím neexistuje síťová cesta k tomuto propagačnímu uzlu. Zkontrolujte rozhraní nebo rozbočovač, oznamte to a zkuste to znovu." }, "reticulumRmapDiscovery": { "sectionTitle": "Objevení RMAP v4", diff --git a/src/renderer/locales/de/translation.json b/src/renderer/locales/de/translation.json index e9548031a..a89f71e39 100644 --- a/src/renderer/locales/de/translation.json +++ b/src/renderer/locales/de/translation.json @@ -4289,7 +4289,8 @@ "syncErrorWithTarget": "{{name}}: {{message}}", "showChatNotice": "Ausbreitungserinnerung im Chat anzeigen", "showChatNoticeAria": "Verbreitungsknoten-Erinnerungsbanner im Chat anzeigen", - "showChatNoticeHint": "Deaktivieren Sie diese Option, um das Chat-Banner auszublenden, das angezeigt wird, wenn kein Ausbreitungsknoten verfügbar ist." + "showChatNoticeHint": "Deaktivieren Sie diese Option, um das Chat-Banner auszublenden, das angezeigt wird, wenn kein Ausbreitungsknoten verfügbar ist.", + "syncPathUnknown": "Noch kein Netzwerkpfad zu diesem Ausbreitungsknoten. Überprüfen Sie Ihre Schnittstellen oder Ihren Hub, kündigen Sie an und versuchen Sie es dann erneut." }, "reticulumRmapDiscovery": { "sectionTitle": "RMAP v4 Entdeckung", diff --git a/src/renderer/locales/en/translation.json b/src/renderer/locales/en/translation.json index 8b37bb611..ac06eb56e 100644 --- a/src/renderer/locales/en/translation.json +++ b/src/renderer/locales/en/translation.json @@ -4597,6 +4597,7 @@ "syncTimedOut": "Propagation sync timed out — the node may be unreachable.", "syncLocalNotSupported": "The local host propagation node cannot be synced over the network like a remote LXMF propagation node.", "syncIdentityUnknown": "Propagation node identity is unknown — wait for an announce or path response, then try again.", + "syncPathUnknown": "No network path to this propagation node yet. Check your interfaces or hub, announce, then try again.", "syncTargetNotPropagationNode": "That destination is reachable on Reticulum but is not an LXMF propagation node (TCP hubs are transport paths only). Add a destination that announces as lxmf.propagation to sync offline messages.", "syncPeeringStampFailed": "Could not create a peering stamp for this propagation node — try again in a moment.", "syncEstablishIdentityMissing": "Propagation sync could not verify the link proof — the node identity was missing. Wait for an announce and try again.", diff --git a/src/renderer/locales/es/translation.json b/src/renderer/locales/es/translation.json index bfb63d803..f5591861e 100644 --- a/src/renderer/locales/es/translation.json +++ b/src/renderer/locales/es/translation.json @@ -4289,7 +4289,8 @@ "syncErrorWithTarget": "{{name}}: {{message}}", "showChatNotice": "Mostrar recordatorio de propagación en el chat", "showChatNoticeAria": "Mostrar el banner de recordatorio del nodo de propagación en el chat", - "showChatNoticeHint": "Desactive esta opción para ocultar el banner de chat que aparece cuando no hay ningún nodo de propagación disponible." + "showChatNoticeHint": "Desactive esta opción para ocultar el banner de chat que aparece cuando no hay ningún nodo de propagación disponible.", + "syncPathUnknown": "Aún no hay ruta de red a este nodo de propagación. Comprueba tus interfaces o hub, anuncia y vuelve a intentarlo." }, "reticulumRmapDiscovery": { "sectionTitle": "Descubrimiento de RMAP v4", diff --git a/src/renderer/locales/fr/translation.json b/src/renderer/locales/fr/translation.json index e6e579302..6b593ab94 100644 --- a/src/renderer/locales/fr/translation.json +++ b/src/renderer/locales/fr/translation.json @@ -4289,7 +4289,8 @@ "syncErrorWithTarget": "{{name}} : {{message}}", "showChatNotice": "Afficher le rappel de propagation dans le chat", "showChatNoticeAria": "Afficher la bannière de rappel du nœud de propagation dans le chat", - "showChatNoticeHint": "Désactivez cette option pour masquer la bannière de chat qui apparaît lorsqu'aucun nœud de propagation n'est disponible." + "showChatNoticeHint": "Désactivez cette option pour masquer la bannière de chat qui apparaît lorsqu'aucun nœud de propagation n'est disponible.", + "syncPathUnknown": "Pas encore de chemin réseau vers ce nœud de propagation. Vérifiez vos interfaces ou votre hub, annoncez, puis réessayez." }, "reticulumRmapDiscovery": { "sectionTitle": "Découverte RMAP v4", diff --git a/src/renderer/locales/id/translation.json b/src/renderer/locales/id/translation.json index 9fff4d89a..b91f0e2f9 100644 --- a/src/renderer/locales/id/translation.json +++ b/src/renderer/locales/id/translation.json @@ -4289,7 +4289,8 @@ "syncErrorWithTarget": "{{name}}: {{message}}", "showChatNotice": "Tampilkan pengingat propagasi di Obrolan", "showChatNoticeAria": "Tampilkan spanduk pengingat simpul propagasi di Obrolan", - "showChatNoticeHint": "Nonaktifkan ini untuk menyembunyikan spanduk Obrolan yang muncul ketika tidak ada node propagasi yang tersedia." + "showChatNoticeHint": "Nonaktifkan ini untuk menyembunyikan spanduk Obrolan yang muncul ketika tidak ada node propagasi yang tersedia.", + "syncPathUnknown": "Belum ada jalur jaringan ke simpul propagasi ini. Periksa antarmuka atau hub Anda, umumkan, lalu coba lagi." }, "reticulumRmapDiscovery": { "sectionTitle": "Penemuan RMAP v4", diff --git a/src/renderer/locales/it/translation.json b/src/renderer/locales/it/translation.json index f6b20dab4..14b7b261a 100644 --- a/src/renderer/locales/it/translation.json +++ b/src/renderer/locales/it/translation.json @@ -4289,7 +4289,8 @@ "syncErrorWithTarget": "{{name}}: {{message}}", "showChatNotice": "Mostra promemoria di propagazione in chat", "showChatNoticeAria": "Mostra il banner di promemoria del nodo di propagazione in Chat", - "showChatNoticeHint": "Disattiva questa opzione per nascondere il banner della chat che appare quando non è disponibile alcun nodo di propagazione." + "showChatNoticeHint": "Disattiva questa opzione per nascondere il banner della chat che appare quando non è disponibile alcun nodo di propagazione.", + "syncPathUnknown": "Ancora nessun percorso di rete a questo nodo di propagazione. Controlla le interfacce o l'hub, annuncia, quindi riprova." }, "reticulumRmapDiscovery": { "sectionTitle": "Scoperta RMAP v4", diff --git a/src/renderer/locales/ja/translation.json b/src/renderer/locales/ja/translation.json index 8e7580f06..a760a1eef 100644 --- a/src/renderer/locales/ja/translation.json +++ b/src/renderer/locales/ja/translation.json @@ -4289,7 +4289,8 @@ "syncErrorWithTarget": "{{name}} : {{message}}", "showChatNotice": "チャットに伝播リマインダーを表示", "showChatNoticeAria": "チャットで伝播ノードのリマインダーバナーを表示する", - "showChatNoticeHint": "これをオフにすると、伝播ノードが利用できないときに表示されるチャットバナーが非表示になります。" + "showChatNoticeHint": "これをオフにすると、伝播ノードが利用できないときに表示されるチャットバナーが非表示になります。", + "syncPathUnknown": "この伝播ノードへのネットワークパスはまだありません。インターフェースまたはハブを確認し、お知らせしてから、もう一度お試しください。" }, "reticulumRmapDiscovery": { "sectionTitle": "RMAP v 4検出", diff --git a/src/renderer/locales/ko/translation.json b/src/renderer/locales/ko/translation.json index 3470d88ea..7fc6fbfaa 100644 --- a/src/renderer/locales/ko/translation.json +++ b/src/renderer/locales/ko/translation.json @@ -4289,7 +4289,8 @@ "syncErrorWithTarget": "{{name}}: {{message}}", "showChatNotice": "Chat에 전파 알림 표시", "showChatNoticeAria": "Chat에 전파 노드 알림 배너 표시", - "showChatNoticeHint": "전파 노드를 사용할 수 없을 때 나타나는 채팅 배너를 숨기려면 이 옵션을 끄십시오." + "showChatNoticeHint": "전파 노드를 사용할 수 없을 때 나타나는 채팅 배너를 숨기려면 이 옵션을 끄십시오.", + "syncPathUnknown": "이 전파 노드에 대한 네트워크 경로가 아직 없습니다. 인터페이스 또는 허브를 확인하고 공지한 다음 다시 시도하십시오." }, "reticulumRmapDiscovery": { "sectionTitle": "RMAP v4 검색", diff --git a/src/renderer/locales/nl/translation.json b/src/renderer/locales/nl/translation.json index f27a86588..0bba9afac 100644 --- a/src/renderer/locales/nl/translation.json +++ b/src/renderer/locales/nl/translation.json @@ -4289,7 +4289,8 @@ "syncErrorWithTarget": "{{name}}: {{message}}", "showChatNotice": "Toon propagatieherinnering in Chat", "showChatNoticeAria": "Toon de herinneringsbanner voor het propagatieknooppunt in Chat", - "showChatNoticeHint": "Schakel dit uit om de chatbanner te verbergen die verschijnt als er geen propagatieknooppunt beschikbaar is." + "showChatNoticeHint": "Schakel dit uit om de chatbanner te verbergen die verschijnt als er geen propagatieknooppunt beschikbaar is.", + "syncPathUnknown": "Nog geen netwerkpad naar dit propagatieknooppunt. Controleer uw interfaces of hub, kondig aan en probeer het opnieuw." }, "reticulumRmapDiscovery": { "sectionTitle": "RMAP v4-detectie", diff --git a/src/renderer/locales/pl/translation.json b/src/renderer/locales/pl/translation.json index a228061d3..c62113169 100644 --- a/src/renderer/locales/pl/translation.json +++ b/src/renderer/locales/pl/translation.json @@ -4293,7 +4293,8 @@ "syncErrorWithTarget": "{{name}}: {{message}}", "showChatNotice": "Pokaż przypomnienie o propagacji na czacie", "showChatNoticeAria": "Pokaż baner przypominający o węźle propagacji na czacie", - "showChatNoticeHint": "Wyłącz tę opcję, aby ukryć baner czatu, który pojawia się, gdy żaden węzeł propagacji nie jest dostępny." + "showChatNoticeHint": "Wyłącz tę opcję, aby ukryć baner czatu, który pojawia się, gdy żaden węzeł propagacji nie jest dostępny.", + "syncPathUnknown": "Nie ma jeszcze ścieżki sieciowej do tego węzła propagacji. Sprawdź interfejsy lub koncentrator, ogłoś, a następnie spróbuj ponownie." }, "reticulumRmapDiscovery": { "sectionTitle": "Wykrywanie RMAP v4", diff --git a/src/renderer/locales/pt-BR/translation.json b/src/renderer/locales/pt-BR/translation.json index 5264b94a4..9a26550b7 100644 --- a/src/renderer/locales/pt-BR/translation.json +++ b/src/renderer/locales/pt-BR/translation.json @@ -4289,7 +4289,8 @@ "syncErrorWithTarget": "{{name}}: {{message}}", "showChatNotice": "Mostrar lembrete de propagação no chat", "showChatNoticeAria": "Mostrar o banner de lembrete do nó de propagação no Chat", - "showChatNoticeHint": "Desative isso para ocultar o banner de bate-papo que aparece quando nenhum nó de propagação está disponível." + "showChatNoticeHint": "Desative isso para ocultar o banner de bate-papo que aparece quando nenhum nó de propagação está disponível.", + "syncPathUnknown": "Nenhum caminho de rede para este nó de propagação ainda. Verifique suas interfaces ou hub, anuncie e tente novamente." }, "reticulumRmapDiscovery": { "sectionTitle": "Descoberta do RMAP v4", diff --git a/src/renderer/locales/ru/translation.json b/src/renderer/locales/ru/translation.json index 2a15866f8..dbd3763f1 100644 --- a/src/renderer/locales/ru/translation.json +++ b/src/renderer/locales/ru/translation.json @@ -4291,7 +4291,8 @@ "syncErrorWithTarget": "{{name}}: {{message}}", "showChatNotice": "Показывать напоминание о распространении в чате", "showChatNoticeAria": "Показывать баннер с напоминанием об узле распространения в чате", - "showChatNoticeHint": "Отключите этот параметр, чтобы скрыть баннер чата, который появляется, когда узел распространения недоступен." + "showChatNoticeHint": "Отключите этот параметр, чтобы скрыть баннер чата, который появляется, когда узел распространения недоступен.", + "syncPathUnknown": "Нет сетевого пути к этому узлу распространения. Проверьте интерфейсы или концентратор, объявите и повторите попытку." }, "reticulumRmapDiscovery": { "sectionTitle": "Обнаружение RMAP v4", diff --git a/src/renderer/locales/tr/translation.json b/src/renderer/locales/tr/translation.json index 27e615665..167023b1a 100644 --- a/src/renderer/locales/tr/translation.json +++ b/src/renderer/locales/tr/translation.json @@ -4289,7 +4289,8 @@ "syncErrorWithTarget": "{{name}}: {{message}}", "showChatNotice": "Chat'te yayılma hatırlatıcısını göster", "showChatNoticeAria": "Yayılma düğümü hatırlatma banner'ını Sohbet'te göster", - "showChatNoticeHint": "Yayılma düğümü bulunmadığında görünen Sohbet başlığını gizlemek için bunu kapatın." + "showChatNoticeHint": "Yayılma düğümü bulunmadığında görünen Sohbet başlığını gizlemek için bunu kapatın.", + "syncPathUnknown": "Bu yayılım düğümüne henüz bir ağ yolu yok. Arayüzlerinizi veya hub'ınızı kontrol edin, anons yapın ve tekrar deneyin." }, "reticulumRmapDiscovery": { "sectionTitle": "RMAP v4 keşfi", diff --git a/src/renderer/locales/uk/translation.json b/src/renderer/locales/uk/translation.json index bf4d3f1dc..f5e19da53 100644 --- a/src/renderer/locales/uk/translation.json +++ b/src/renderer/locales/uk/translation.json @@ -4291,7 +4291,8 @@ "syncErrorWithTarget": "{{name}}: {{message}}", "showChatNotice": "Показати нагадування про поширення в чаті", "showChatNoticeAria": "Показати банер нагадування про вузол поширення в чаті", - "showChatNoticeHint": "Вимкніть, щоб приховати банер чату, який з'являється, коли вузол поширення недоступний." + "showChatNoticeHint": "Вимкніть, щоб приховати банер чату, який з'являється, коли вузол поширення недоступний.", + "syncPathUnknown": "Ще немає мережевого шляху до цього вузла поширення. Перевірте інтерфейси або концентратор, оголосіть, а потім повторіть спробу." }, "reticulumRmapDiscovery": { "sectionTitle": "Виявлення RMAP v4", diff --git a/src/renderer/locales/zh/translation.json b/src/renderer/locales/zh/translation.json index b933c7ed5..d6d934b2c 100644 --- a/src/renderer/locales/zh/translation.json +++ b/src/renderer/locales/zh/translation.json @@ -4289,7 +4289,8 @@ "syncErrorWithTarget": "{{name}} : {{message}}", "showChatNotice": "在聊天中显示传播提醒", "showChatNoticeAria": "在聊天中显示传播节点提醒横幅", - "showChatNoticeHint": "关闭此选项可隐藏在没有可用传播节点时显示的聊天横幅。" + "showChatNoticeHint": "关闭此选项可隐藏在没有可用传播节点时显示的聊天横幅。", + "syncPathUnknown": "还没有到此传播节点的网络路径。请检查您的接口或集线器,宣布,然后重试。" }, "reticulumRmapDiscovery": { "sectionTitle": "RMAP v4发现",