diff --git a/docs/user/t3-connect-troubleshooting.md b/docs/user/t3-connect-troubleshooting.md index 27af390b81ef..4abe8bcc5d16 100644 --- a/docs/user/t3-connect-troubleshooting.md +++ b/docs/user/t3-connect-troubleshooting.md @@ -99,7 +99,8 @@ Fix: ## Sign-in, account, or link problems -T3 Connect does not have an active link between your account and the host. +This device's T3 Connect sign-in is not ready yet, or T3 Connect does not have +an active link between your account and the host. - **The host is linked to a different account.** Sign in to the same T3 Connect account on this device and on the host. On the host, run `t3 connect logout`, @@ -111,6 +112,9 @@ T3 Connect does not have an active link between your account and the host. `t3 service restart` on the host, or restart `t3 serve`. - **This device's sign-in expired, or the host no longer accepts this device.** Sign out of T3 Connect and sign in again. +- **This device is still loading its T3 Connect sign-in.** Wait a moment; + T3 Code retries on its own. If it keeps waiting, reload or restart T3 Code on + this device. - **Your account reached its tunnel limit.** Remove an environment you do not use from your T3 account's T3 Connect page, or [link a paid T3 Chat account](./remote-access.md#more-machines-with-t3-chat) diff --git a/packages/client-runtime/src/connection/diagnosis.test.ts b/packages/client-runtime/src/connection/diagnosis.test.ts index 5d396888eb59..93cc54bd044d 100644 --- a/packages/client-runtime/src/connection/diagnosis.test.ts +++ b/packages/client-runtime/src/connection/diagnosis.test.ts @@ -8,6 +8,8 @@ import { presentConnectionDiagnosis } from "./diagnosis.ts"; import { mapManagedRelayError, mapRemoteEnvironmentError } from "./errors.ts"; import { AVAILABLE_CONNECTION_STATE, + ConnectionBlockedError, + ConnectionTransientError, type ConnectionAttemptError, type SupervisorConnectionState, } from "./model.ts"; @@ -128,6 +130,30 @@ describe("presentConnectionDiagnosis", () => { expect(origin?.hops.map((hop) => hop.hop)).toEqual(["device", "relay", "tunnel", "server"]); }); + it("blames this device, not the host, while its own T3 Connect sign-in loads", () => { + const failure = new ConnectionTransientError({ + reason: "session-pending", + detail: "Waiting for the T3 Connect sign-in to load.", + }); + const diagnosis = diagnose(backoff(failure)); + expect(diagnosis?.headline).not.toContain("did not answer"); + expect(diagnosis?.hops.find((hop) => hop.result === "failed")?.hop).toBe("device"); + expect(diagnosis?.steps.join(" ")).not.toContain("Mac mini"); + expect(diagnosis?.moreInfoUrl).toContain("sign-in-account-or-link-problems"); + // A host with a LAN route as well uses the direct route, still authorized through T3 Connect. + expect(diagnose(backoff(failure), "direct")?.moreInfoUrl).toContain( + "sign-in-account-or-link-problems", + ); + }); + + it("does not report hops as working when the failure names none", () => { + const failure = new ConnectionBlockedError({ + reason: "authentication", + detail: "Your cloud sign-in changed. Sign in again to authorize the environment.", + }); + expect(results(backoff(failure))).not.toContainEqual(expect.stringMatching(/:ok$/)); + }); + it("keeps the relay health status out of a failure that has its own cause", () => { const failure = mapRemoteEnvironmentError( new RemoteEnvironmentAuthFetchError({ message: "Failed to fetch", cause: new TypeError() }), diff --git a/packages/client-runtime/src/connection/diagnosis.ts b/packages/client-runtime/src/connection/diagnosis.ts index a3de100766a7..4272943e0daa 100644 --- a/packages/client-runtime/src/connection/diagnosis.ts +++ b/packages/client-runtime/src/connection/diagnosis.ts @@ -26,7 +26,7 @@ export interface ConnectionDiagnosis { readonly hops: ReadonlyArray; readonly steps: ReadonlyArray; readonly facts: ReadonlyArray; - /** The troubleshooting guide section for this failure. Null for direct routes. */ + /** The troubleshooting guide section for this failure. Null for direct routes, except a pending T3 Connect sign-in. */ readonly moreInfoUrl: string | null; } @@ -46,7 +46,15 @@ export interface ConnectionDiagnosisInput { interface Located { readonly hop: ConnectionHop; - readonly cause: ConnectionFailureCause | "offline" | "outdated" | "released" | null; + readonly cause: + | ConnectionFailureCause + | "offline" + | "outdated" + | "released" + | "session_pending" + | null; + /** The failure named no hop, so the hops before this one were never probed. */ + readonly guessed?: true; } const NOT_ANSWERING: ReadonlySet = new Set([ @@ -62,6 +70,9 @@ function locate(input: ConnectionDiagnosisInput): Located | null { if (failure?._tag === "ConnectionBlockedError" && failure.serverUpdateRequired) { return { hop: "server", cause: "outdated" }; } + if (failure?.reason === "session-pending") { + return { hop: "device", cause: "session_pending" }; + } if (failure?.hop !== undefined) { return { hop: failure.hop, cause: failure.failureCause ?? null }; } @@ -71,7 +82,11 @@ function locate(input: ConnectionDiagnosisInput): Located | null { failure.reason === "timeout" || failure.reason === "transport"; const via = failure.via ?? input.via; - return { hop: unreachable && via === "relay" ? "tunnel" : "server", cause: null }; + return { + hop: unreachable && via === "relay" ? "tunnel" : "server", + cause: null, + guessed: true, + }; } const status = input.relayStatus; if (status?.status !== "offline") return null; @@ -84,6 +99,8 @@ function headline(host: string, located: Located, failure: ConnectionAttemptErro switch (located.cause) { case "offline": return "This device is offline"; + case "session_pending": + return "This device is still loading its T3 Connect sign-in"; case "outdated": return `${host} runs an older T3 Code`; case "clock_skew": @@ -141,6 +158,11 @@ function steps(host: string, located: Located, failure: ConnectionAttemptError | switch (located.cause) { case "offline": return ["Connect this device to the internet. T3 Code reconnects on its own."]; + case "session_pending": + return [ + "Wait a moment. T3 Code retries on its own.", + "If it keeps waiting, reload or restart T3 Code on this device.", + ]; case "outdated": return [`Update T3 Code on ${host}.`]; case "clock_skew": @@ -220,6 +242,7 @@ function helpSection( return "the-host-runs-an-older-t3-code"; case "clock_skew": return "the-clock-is-off"; + case "session_pending": case "not_linked": case "tunnel_not_enabled": return "sign-in-account-or-link-problems"; @@ -300,7 +323,12 @@ export function presentConnectionDiagnosis( const hops = order.map((hop, index): ConnectionDiagnosisHop => ({ hop, label: hopLabel(hop, host), - result: index < failedIndex ? "ok" : index === failedIndex ? "failed" : "unchecked", + result: + index === failedIndex + ? "failed" + : index < failedIndex && located.guessed !== true + ? "ok" + : "unchecked", })); // The relay health check only explains the state when no attempt has failed yet. @@ -329,7 +357,10 @@ export function presentConnectionDiagnosis( hops, steps: steps(host, located, failure), facts, - moreInfoUrl: relayPath ? t3ConnectTroubleshootingUrl(helpSection(located, failure)) : null, + moreInfoUrl: + relayPath || located.cause === "session_pending" + ? t3ConnectTroubleshootingUrl(helpSection(located, failure)) + : null, }; }