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