From 26e88355e168724620a5bbb2fbccd03955bb2442 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Fri, 10 Jul 2026 03:22:57 +0300 Subject: [PATCH] fix(desktop): relay host status, display, and server-side LAN candidate - Probe relay hosts through a throwaway E2EE tunnel in the host switcher instead of an HTTP probe against the relay:// pseudo-URL, which always reported Unreachable - Show 'via OpenChamber Relay' for relay hosts in the switcher and the servers list instead of the raw relay:// pseudo-URL; hide the URL-centric edit action for relay hosts (saving it would drop the tunnel descriptor) - Pairing LAN candidate prefers the address the requesting client actually reached the server on; interface scanning could pick an unroutable virtual bridge (docker0), producing links whose LAN leg silently failed and forced devices onto the relay --- .../desktop/DesktopHostSwitcher.tsx | 11 +++++++- .../remote-instances/RemoteInstancesPage.tsx | 17 +++++++---- packages/web/server/index.js | 28 +++++++++++++++---- .../web/server/lib/opencode/core-routes.js | 2 +- 4 files changed, 46 insertions(+), 12 deletions(-) diff --git a/packages/ui/src/components/desktop/DesktopHostSwitcher.tsx b/packages/ui/src/components/desktop/DesktopHostSwitcher.tsx index 49f4b560..d444379b 100644 --- a/packages/ui/src/components/desktop/DesktopHostSwitcher.tsx +++ b/packages/ui/src/components/desktop/DesktopHostSwitcher.tsx @@ -450,6 +450,13 @@ export function DesktopHostSwitcherDialog({ const localClientToken = await getLocalClientToken(); const results = await Promise.all( hosts.map(async (h) => { + // Relay hosts have no HTTP address to probe — check reachability + // through a throwaway E2EE tunnel instead. + if (h.relay) { + const startedAt = performance.now(); + const ok = await probeRelayHost(h.relay).catch(() => false); + return [h.id, { status: ok ? ('ok' as const) : ('unreachable' as const), latencyMs: Math.round(performance.now() - startedAt) } satisfies HostStatus] as const; + } const url = normalizeHostUrl(isElectronShell() ? getDesktopHostApiUrl(h) : h.url); if (!url) { return [h.id, { status: 'unreachable' as const, latencyMs: 0 } satisfies HostStatus] as const; @@ -898,7 +905,9 @@ export function DesktopHostSwitcherDialog({ const displayLabel = host.id === LOCAL_HOST_ID ? t('desktopHostSwitcher.instance.local') : redactSensitiveUrl(host.label); - const displayUrl = redactSensitiveUrl(effectiveUrl); + // Relay hosts have a relay:// pseudo-URL that means nothing to a + // person — say how the connection works instead. + const displayUrl = host.relay ? t('mobile.connect.relay.badge') : redactSensitiveUrl(effectiveUrl); return (
{

{redactSensitiveUrl(host.label)}

{directDefaultHostId === host.id ? {t('desktopHostSwitcher.header.default')} : null}
-

{redactSensitiveUrl(host.apiUrl || host.url)}

+

+ {host.relay ? t('mobile.connect.relay.badge') : redactSensitiveUrl(host.apiUrl || host.url)} +

- + {/* The edit form is URL/token-centric; saving it would drop a + relay host's tunnel descriptor. Relay hosts are re-imported + via a fresh pairing link instead. */} + {host.relay ? null : ( + + )}