fix(relay): share the diagnostics payload with the settings pane

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 <noreply@anthropic.com>
This commit is contained in:
Tan Vir
2026-09-09 07:40:21 +06:00
co-authored by Claude Fable 5.1
parent 11ad3776f7
commit fb135c95f7
4 changed files with 34 additions and 32 deletions
@@ -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<typeof setTimeout> | 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
@@ -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))
@@ -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<MobileRelayDiagnosticsPayload> {
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
})
}
@@ -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