authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-07-16 09:22:07+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-16 09:22:07+02:00
logb98233702c88fd7a4312b231e68b089d5f3b0e80
tree38e7877edafc7e189fff8cd53cd6ea563019a854
parentecb35f75839925dd1ac6f854efeef5c9e291d11b
signature Signed by PGP key B5690EEEBB952194

[codex] Fix onboarding connection status (#4001)

Co-authored-by: codex <codex@users.noreply.github.com>

5 files changed, 196 insertions(+), 58 deletions(-)

apps/web/src/components/cloud/CloudEnvironmentConnectList.tsx+73-40
......@@ -1,5 +1,6 @@
11import { findErrorTraceId } from "@t3tools/client-runtime/errors";
22import {
3 type EnvironmentConnectionPresentation,
34 RelayConnectionRegistration,
45 RelayConnectionTarget,
56} from "@t3tools/client-runtime/connection";
......@@ -10,7 +11,7 @@ import {
1011import type { EnvironmentId } from "@t3tools/contracts";
1112import type { RelayClientEnvironmentRecord } from "@t3tools/contracts/relay";
1213import * as Option from "effect/Option";
13import { type ReactNode, useCallback, useEffect, useMemo, useState } from "react";
14import { type ReactNode, useCallback, useEffect, useState } from "react";
1415
1516import { environmentCatalog } from "~/connection/catalog";
1617import { cn } from "~/lib/utils";
......@@ -22,6 +23,12 @@ import { ITEM_ROW_CLASSNAME, ITEM_ROW_INNER_CLASSNAME } from "../settings/itemRo
2223import { Button } from "../ui/button";
2324import { Skeleton } from "../ui/skeleton";
2425import { toastManager } from "../ui/toast";
26import { presentSavedCloudEnvironmentConnection } from "./cloudEnvironmentConnectionPresentation";
27
28export interface SavedCloudEnvironmentConnection {
29 readonly environmentId: EnvironmentId;
30 readonly connection: EnvironmentConnectionPresentation;
31}
2532
2633export function RemoteEnvironmentRowsSkeleton() {
2734 return (
......@@ -40,19 +47,19 @@ export function RemoteEnvironmentRowsSkeleton() {
4047/**
4148 * The user's T3 Connect environments from relay discovery, each with a
4249 * Connect button. The primary environment is always excluded; already-saved
43 * environments are hidden unless `showSavedAsConnected` renders them as
44 * connected instead (used by onboarding, where the full device mesh should be
45 * visible).
50 * environments are hidden unless `showSavedEnvironments` renders them with
51 * their live connection state (used by onboarding, where the full device mesh
52 * should be visible).
4653 */
4754export function CloudEnvironmentConnectRows({
4855 primaryEnvironmentId,
49 savedEnvironmentIds,
50 showSavedAsConnected = false,
56 savedEnvironments,
57 showSavedEnvironments = false,
5158 empty = null,
5259}: {
5360 readonly primaryEnvironmentId: EnvironmentId | null;
54 readonly savedEnvironmentIds: ReadonlyArray<EnvironmentId>;
55 readonly showSavedAsConnected?: boolean;
61 readonly savedEnvironments: ReadonlyArray<SavedCloudEnvironmentConnection>;
62 readonly showSavedEnvironments?: boolean;
5663 readonly empty?: ReactNode;
5764}) {
5865 const environmentsState = useRelayEnvironmentDiscovery();
......@@ -77,7 +84,9 @@ export function CloudEnvironmentConnectRows({
7784 const [connectingEnvironmentId, setConnectingEnvironmentId] = useState<EnvironmentId | null>(
7885 null,
7986 );
80 const savedIds = useMemo(() => new Set(savedEnvironmentIds), [savedEnvironmentIds]);
87 const savedById = new Map(
88 savedEnvironments.map((environment) => [environment.environmentId, environment]),
89 );
8190
8291 useEffect(() => {
8392 void refreshRelayEnvironments();
......@@ -90,8 +99,8 @@ export function CloudEnvironmentConnectRows({
9099 if (result._tag === "Success") {
91100 toastManager.add({
92101 type: "success",
93 title: "Environment connected",
94 description: `${environment.label} is available through T3 Connect.`,
102 title: "Environment added",
103 description: `Connecting to ${environment.label} through T3 Connect.`,
95104 });
96105 return;
97106 }
......@@ -121,10 +130,10 @@ export function CloudEnvironmentConnectRows({
121130 const visibleEnvironments = [...environmentsState.environments.values()].filter(
122131 ({ environment }) =>
123132 environment.environmentId !== primaryEnvironmentId &&
124 (showSavedAsConnected || !savedIds.has(environment.environmentId)),
133 (showSavedEnvironments || !savedById.has(environment.environmentId)),
125134 );
126135
127 const standalone = showSavedAsConnected || savedEnvironmentIds.length === 0;
136 const standalone = showSavedEnvironments || savedEnvironments.length === 0;
128137
129138 if (
130139 standalone &&
......@@ -163,31 +172,57 @@ export function CloudEnvironmentConnectRows({
163172 }
164173
165174 return visibleEnvironments.map(({ environment, availability, error }) => {
166 const alreadyConnected = savedIds.has(environment.environmentId);
175 const savedEnvironment = savedById.get(environment.environmentId);
176 const savedConnection = savedEnvironment
177 ? presentSavedCloudEnvironmentConnection(savedEnvironment.connection)
178 : null;
179 const dotClassName = savedConnection
180 ? savedConnection.tone === "connected"
181 ? "bg-success"
182 : savedConnection.tone === "connecting"
183 ? "bg-warning"
184 : savedConnection.tone === "error"
185 ? "bg-destructive"
186 : "bg-muted-foreground/35"
187 : availability === "online"
188 ? "bg-success"
189 : availability === "error"
190 ? "bg-destructive"
191 : availability === "checking"
192 ? "bg-warning"
193 : "bg-muted-foreground/35";
194 const statusText = savedConnection
195 ? savedConnection.statusText
196 : availability === "online"
197 ? "Available · Relay online"
198 : availability === "offline"
199 ? "Available · Relay offline"
200 : availability === "checking"
201 ? "Available · Checking relay status…"
202 : (Option.getOrNull(error)?.message ?? "Available · Relay status unavailable");
167203 return (
168204 <div key={environment.environmentId} className={ITEM_ROW_CLASSNAME}>
169205 <div className={ITEM_ROW_INNER_CLASSNAME}>
170206 <div className="min-w-0">
171207 <div className="flex items-center gap-2">
172208 <ConnectionStatusDot
173 dotClassName={
174 availability === "online"
175 ? "bg-success"
176 : availability === "error"
177 ? "bg-destructive"
178 : availability === "checking"
179 ? "bg-warning"
180 : "bg-muted-foreground/35"
209 dotClassName={dotClassName}
210 pingClassName={
211 savedConnection?.tone === "connecting" ||
212 (savedConnection === null && availability === "checking")
213 ? "bg-warning/60 duration-2000"
214 : null
181215 }
182 pingClassName={availability === "checking" ? "bg-warning/60 duration-2000" : null}
183216 tooltipText={
184 availability === "online"
185 ? "Relay online"
186 : availability === "offline"
187 ? "Relay offline"
188 : availability === "checking"
189 ? "Checking relay status"
190 : (Option.getOrNull(error)?.message ?? "Relay status unavailable")
217 savedConnection
218 ? savedConnection.statusText
219 : availability === "online"
220 ? "Relay online"
221 : availability === "offline"
222 ? "Relay offline"
223 : availability === "checking"
224 ? "Checking relay status"
225 : (Option.getOrNull(error)?.message ?? "Relay status unavailable")
191226 }
192227 />
193228 <p className="truncate text-sm font-medium">{environment.label}</p>
......@@ -195,21 +230,19 @@ export function CloudEnvironmentConnectRows({
195230 <p
196231 className={cn(
197232 "mt-1 truncate text-xs",
198 availability === "error" ? "text-destructive" : "text-muted-foreground",
233 savedConnection?.tone === "error" ||
234 (savedConnection?.tone === "connecting" && savedEnvironment?.connection.error) ||
235 (savedConnection === null && availability === "error")
236 ? "text-destructive"
237 : "text-muted-foreground",
199238 )}
200239 >
201 {availability === "online"
202 ? "Available · Relay online"
203 : availability === "offline"
204 ? "Available · Relay offline"
205 : availability === "checking"
206 ? "Available · Checking relay status…"
207 : (Option.getOrNull(error)?.message ?? "Available · Relay status unavailable")}
240 {statusText}
208241 </p>
209242 </div>
210 {alreadyConnected ? (
243 {savedConnection ? (
211244 <Button size="sm" variant="outline" disabled>
212 Connected
245 {savedConnection.buttonLabel}
213246 </Button>
214247 ) : (
215248 <Button
apps/web/src/components/cloud/ConnectOnboardingDialog.tsx+5-9
......@@ -1,7 +1,7 @@
11import { useAuth } from "@clerk/react";
22import { AuthAdministrativeScopes, AuthRelayWriteScope } from "@t3tools/contracts";
33import { CheckIcon } from "lucide-react";
4import { useEffect, useMemo, useRef, useState } from "react";
4import { useEffect, useRef, useState } from "react";
55
66import {
77 CONNECT_ONBOARDING_OPT_OUT_STORAGE_KEY,
......@@ -411,20 +411,16 @@ function OnboardingToggleRow({
411411function DevicesStep() {
412412 const { environments } = useEnvironments();
413413 const primaryEnvironment = usePrimaryEnvironment();
414 const savedEnvironmentIds = useMemo(
415 () =>
416 environments
417 .filter((environment) => environment.entry.target._tag !== "PrimaryConnectionTarget")
418 .map((environment) => environment.environmentId),
419 [environments],
414 const savedEnvironments = environments.filter(
415 (environment) => environment.entry.target._tag !== "PrimaryConnectionTarget",
420416 );
421417
422418 return (
423419 <div className="overflow-hidden rounded-lg border">
424420 <CloudEnvironmentConnectRows
425421 primaryEnvironmentId={primaryEnvironment?.environmentId ?? null}
426 savedEnvironmentIds={savedEnvironmentIds}
427 showSavedAsConnected
422 savedEnvironments={savedEnvironments}
423 showSavedEnvironments
428424 empty={
429425 <p className="px-4 py-6 text-center text-sm text-muted-foreground">
430426 No other environments are published to your account yet. Publish one from another device
apps/web/src/components/cloud/cloudEnvironmentConnectionPresentation.test.ts created+55
......@@ -0,0 +1,55 @@
1import type { EnvironmentConnectionPresentation } from "@t3tools/client-runtime/connection";
2import { describe, expect, it } from "vite-plus/test";
3
4import { presentSavedCloudEnvironmentConnection } from "./cloudEnvironmentConnectionPresentation";
5
6function connection(
7 phase: EnvironmentConnectionPresentation["phase"],
8 error: string | null = null,
9): EnvironmentConnectionPresentation {
10 return { phase, error, traceId: null };
11}
12
13describe("saved cloud environment connection presentation", () => {
14 it("only labels a live connection as connected", () => {
15 expect(presentSavedCloudEnvironmentConnection(connection("connected"))).toEqual({
16 buttonLabel: "Connected",
17 statusText: "Connected",
18 tone: "connected",
19 });
20
21 expect(presentSavedCloudEnvironmentConnection(connection("connecting"))).toEqual({
22 buttonLabel: "Connecting…",
23 statusText: "Connecting...",
24 tone: "connecting",
25 });
26 });
27
28 it("surfaces a failed attempt while the supervisor reconnects", () => {
29 expect(
30 presentSavedCloudEnvironmentConnection(
31 connection("reconnecting", "Relay environment endpoint is unavailable."),
32 ),
33 ).toEqual({
34 buttonLabel: "Reconnecting…",
35 statusText:
36 "Failed to connect. Reconnecting... Reason: Relay environment endpoint is unavailable.",
37 tone: "connecting",
38 });
39 });
40
41 it.each([
42 ["error", "Connection failed", "Connection failed. Reason: Access denied.", "error"],
43 ["offline", "Offline", "Offline", "idle"],
44 ["available", "Not connected", "Available", "idle"],
45 ] as const)(
46 "presents %s without claiming the environment is connected",
47 (phase, buttonLabel, statusText, tone) => {
48 expect(
49 presentSavedCloudEnvironmentConnection(
50 connection(phase, phase === "error" ? "Access denied." : null),
51 ),
52 ).toEqual({ buttonLabel, statusText, tone });
53 },
54 );
55});
apps/web/src/components/cloud/cloudEnvironmentConnectionPresentation.ts created+58
......@@ -0,0 +1,58 @@
1import {
2 connectionStatusText,
3 type EnvironmentConnectionPresentation,
4} from "@t3tools/client-runtime/connection";
5
6export interface SavedCloudEnvironmentConnectionPresentation {
7 readonly buttonLabel: string;
8 readonly statusText: string;
9 readonly tone: "connected" | "connecting" | "error" | "idle";
10}
11
12/**
13 * Present the live supervisor state for an environment that is already in the
14 * connection catalog. Catalog membership only means the environment is saved;
15 * it does not mean the connection attempt succeeded.
16 */
17export function presentSavedCloudEnvironmentConnection(
18 connection: EnvironmentConnectionPresentation,
19): SavedCloudEnvironmentConnectionPresentation {
20 switch (connection.phase) {
21 case "connected":
22 return {
23 buttonLabel: "Connected",
24 statusText: connectionStatusText(connection),
25 tone: "connected",
26 };
27 case "connecting":
28 return {
29 buttonLabel: "Connecting…",
30 statusText: connectionStatusText(connection),
31 tone: "connecting",
32 };
33 case "reconnecting":
34 return {
35 buttonLabel: "Reconnecting…",
36 statusText: connectionStatusText(connection),
37 tone: "connecting",
38 };
39 case "error":
40 return {
41 buttonLabel: "Connection failed",
42 statusText: connectionStatusText(connection),
43 tone: "error",
44 };
45 case "offline":
46 return {
47 buttonLabel: "Offline",
48 statusText: connectionStatusText(connection),
49 tone: "idle",
50 };
51 case "available":
52 return {
53 buttonLabel: "Not connected",
54 statusText: connectionStatusText(connection),
55 tone: "idle",
56 };
57 }
58}
apps/web/src/components/settings/ConnectionsSettings.tsx+5-9
......@@ -1672,18 +1672,18 @@ function EmptyRemoteEnvironments({ cloudEnabled = true }: { readonly cloudEnable
16721672
16731673function CloudRemoteEnvironmentRows({
16741674 primaryEnvironmentId,
1675 savedEnvironmentIds,
1675 savedEnvironments,
16761676}: {
16771677 readonly primaryEnvironmentId: EnvironmentId | null;
1678 readonly savedEnvironmentIds: ReadonlyArray<EnvironmentId>;
1678 readonly savedEnvironments: ReadonlyArray<EnvironmentPresentation>;
16791679}) {
16801680 return hasCloudPublicConfig() ? (
16811681 <CloudEnvironmentConnectRows
16821682 primaryEnvironmentId={primaryEnvironmentId}
1683 savedEnvironmentIds={savedEnvironmentIds}
1683 savedEnvironments={savedEnvironments}
16841684 empty={<EmptyRemoteEnvironments />}
16851685 />
1686 ) : savedEnvironmentIds.length === 0 ? (
1686 ) : savedEnvironments.length === 0 ? (
16871687 <EmptyRemoteEnvironments cloudEnabled={false} />
16881688 ) : null;
16891689}
......@@ -1713,10 +1713,6 @@ export function ConnectionsSettings() {
17131713 .toSorted((left, right) => left.label.localeCompare(right.label)),
17141714 [environments],
17151715 );
1716 const savedEnvironmentIds = useMemo(
1717 () => savedEnvironments.map((environment) => environment.environmentId),
1718 [savedEnvironments],
1719 );
17201716 const savedDesktopSshEnvironmentsByAlias = useMemo(
17211717 () =>
17221718 savedEnvironments.reduce<Record<string, EnvironmentPresentation>>(
......@@ -3363,7 +3359,7 @@ export function ConnectionsSettings() {
33633359 ))}
33643360 <CloudRemoteEnvironmentRows
33653361 primaryEnvironmentId={primaryEnvironmentId}
3366 savedEnvironmentIds={savedEnvironmentIds}
3362 savedEnvironments={savedEnvironments}
33673363 />
33683364 </SettingsSection>
33693365 </SettingsPageContainer>