From fb135c95f7146965882cb4caea019bd6a0dcf07c Mon Sep 17 00:00:00 2001 From: Tan Vir Date: Wed, 9 Sep 2026 07:40:21 +0600 Subject: [PATCH] fix(relay): share the diagnostics payload with the settings pane MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Settings → Mobile pane had its own inline "Copy diagnostics" builder that still wrote the selected address into a payload users are asked to share, and lacked the relay status and app version. Both entry points now call one collectMobileRelayDiagnosticsPayload, so they emit one shape with no network identifier. RelayDrainRetrySchedule reuses relayRetryDelayMs instead of carrying a second copy of the same backoff constants and formula. Co-Authored-By: Claude Fable 5.1 --- .../relay/relay-drain-retry-schedule.ts | 7 ++---- .../src/components/mobile/MobilePage.tsx | 16 +++----------- .../mobile-relay-diagnostics-payload.ts | 21 ++++++++++++++++++ .../src/components/settings/MobilePane.tsx | 22 +++++++------------ 4 files changed, 34 insertions(+), 32 deletions(-) diff --git a/src/main/runtime/relay/relay-drain-retry-schedule.ts b/src/main/runtime/relay/relay-drain-retry-schedule.ts index 0aa945a7a2b..609b6531a56 100644 --- a/src/main/runtime/relay/relay-drain-retry-schedule.ts +++ b/src/main/runtime/relay/relay-drain-retry-schedule.ts @@ -1,5 +1,4 @@ -const RETRY_BASE_MS = 1_000 -const RETRY_MAX_MS = 5 * 60_000 +import { relayRetryDelayMs } from './relay-retry-delay' export class RelayDrainRetrySchedule { private timer: ReturnType | null = null @@ -15,10 +14,8 @@ export class RelayDrainRetrySchedule { if (this.timer) { return } - const exponent = Math.min(this.attempt, Math.ceil(Math.log2(RETRY_MAX_MS / RETRY_BASE_MS))) - const capMs = Math.min(RETRY_MAX_MS, RETRY_BASE_MS * 2 ** exponent) + const jitterMs = relayRetryDelayMs(this.attempt, this.random) this.attempt++ - const jitterMs = Math.floor(this.random() * (capMs + 1)) this.timer = setTimeout( () => { this.timer = null diff --git a/src/renderer/src/components/mobile/MobilePage.tsx b/src/renderer/src/components/mobile/MobilePage.tsx index f4ce3974b3e..9cb01e208ff 100644 --- a/src/renderer/src/components/mobile/MobilePage.tsx +++ b/src/renderer/src/components/mobile/MobilePage.tsx @@ -2,7 +2,6 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react import { toast } from 'sonner' import { useMountedRef } from '@/hooks/useMountedRef' import { useAppStore } from '@/store' -import { resolveClientEnvironmentInfo } from '@/lib/client-environment-info' import type { Platform, StepIndex } from './MobileHero' import type { IosChannel } from './mobile-platform-copy' import type { MobileNetworkInterface } from '../settings/mobile-network-interface-selection' @@ -20,7 +19,7 @@ import { useMobilePairingQrInvalidation } from './use-mobile-pairing-qr-invalida import { useMobileInstallActions } from './use-mobile-install-actions' import { useMobilePagePairedDevices } from './use-mobile-page-paired-devices' import type { MobileRelayMintFailure } from '../../../../shared/mobile-relay-mint-failure' -import { buildMobileRelayDiagnosticsPayload } from './mobile-relay-diagnostics-payload' +import { collectMobileRelayDiagnosticsPayload } from './mobile-relay-diagnostics-payload' import { type MobilePairingAddressChange, useMobilePairingAddressPreference @@ -149,18 +148,9 @@ export default function MobilePage(): React.JSX.Element { return } // Why: users share this payload — an address (selected or relay cell) would leak a LAN/Tailscale IP or hostname. - const [relayStatus, environment] = await Promise.all([ - window.api.mobile - .getRelayStatus() - .then((detail) => detail.status) - .catch(() => 'offline' as const), - resolveClientEnvironmentInfo() - ]) - const payload = buildMobileRelayDiagnosticsPayload({ + const payload = await collectMobileRelayDiagnosticsPayload({ connectionMode, - failure: relayMintFailure, - relayStatus, - appVersion: environment.appVersion + failure: relayMintFailure }) try { await window.api.ui.writeClipboardText(JSON.stringify(payload, null, 2)) diff --git a/src/renderer/src/components/mobile/mobile-relay-diagnostics-payload.ts b/src/renderer/src/components/mobile/mobile-relay-diagnostics-payload.ts index 2b94e6d3309..8ccac2eedc8 100644 --- a/src/renderer/src/components/mobile/mobile-relay-diagnostics-payload.ts +++ b/src/renderer/src/components/mobile/mobile-relay-diagnostics-payload.ts @@ -1,6 +1,7 @@ import type { MobilePairingConnectionMode } from '../../../../shared/mobile-pairing-connection-mode' import type { MobileRelayMintFailure } from '../../../../shared/mobile-relay-mint-failure' import type { MobileRelayStatus } from '../../../../shared/mobile-relay-status' +import { resolveClientEnvironmentInfo } from '@/lib/client-environment-info' export type MobileRelayDiagnosticsPayload = { kind: 'mobile_pairing_relay_failure' @@ -28,3 +29,23 @@ export function buildMobileRelayDiagnosticsPayload(args: { at: new Date().toISOString() } } + +// Why here rather than at each call site: both "Copy diagnostics" buttons must +// fetch the same two fields the same way, and MobilePane sits at the line ceiling. +export async function collectMobileRelayDiagnosticsPayload(args: { + connectionMode: MobilePairingConnectionMode + failure: MobileRelayMintFailure +}): Promise { + const [relayStatus, environment] = await Promise.all([ + window.api.mobile + .getRelayStatus() + .then((detail) => detail.status) + .catch(() => 'offline' as const), + resolveClientEnvironmentInfo() + ]) + return buildMobileRelayDiagnosticsPayload({ + ...args, + relayStatus, + appVersion: environment.appVersion + }) +} diff --git a/src/renderer/src/components/settings/MobilePane.tsx b/src/renderer/src/components/settings/MobilePane.tsx index 0e51b677ffc..1c82adf25cb 100644 --- a/src/renderer/src/components/settings/MobilePane.tsx +++ b/src/renderer/src/components/settings/MobilePane.tsx @@ -15,6 +15,7 @@ import { MobileAutoRestoreFitSection } from './MobileAutoRestoreFitSection' import { MobilePairingConnectionOptions } from './MobilePairingConnectionOptions' import { MobilePairingSetupSection } from './MobilePairingSetupSection' import { MobileRelayMintFailureNotice } from '../mobile/mobile-relay-mint-failure-notice' +import { collectMobileRelayDiagnosticsPayload } from '../mobile/mobile-relay-diagnostics-payload' import { WindowsFirewallNotice } from '../mobile/WindowsFirewallNotice' import { translate } from '@/i18n/i18n' import { @@ -300,20 +301,13 @@ export function MobilePane(): React.JSX.Element { if (relayMintFailure == null) { return } + // Why: users share this payload, so it carries no address (selected or relay cell). + const payload = await collectMobileRelayDiagnosticsPayload({ + connectionMode, + failure: relayMintFailure + }) try { - await window.api.ui.writeClipboardText( - JSON.stringify( - { - kind: 'mobile_pairing_relay_failure', - preferredConnectionMode: connectionMode, - failure: relayMintFailure, - selectedAddress: selectedAddress ?? null, - at: new Date().toISOString() - }, - null, - 2 - ) - ) + await window.api.ui.writeClipboardText(JSON.stringify(payload, null, 2)) if (mountedRef.current) { toast.success( translate('auto.components.settings.MobilePane.diagnosticsCopied', 'Diagnostics copied') @@ -329,7 +323,7 @@ export function MobilePane(): React.JSX.Element { ) } } - }, [connectionMode, mountedRef, relayMintFailure, selectedAddress]) + }, [connectionMode, mountedRef, relayMintFailure]) // Why: another window can persist a different path; the shared hook syncs // connectionMode here without routing through changeConnectionMode. Treat