authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-07-19 19:23:53+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-19 19:23:53+02:00
log2b180a2b2d307a902fa63c8de3d7a42ea5e1b9c5
tree981ef95c00130b19d74b7f4a09d1cdd754eb0111
parent7a820abfddbf77166d16614bb9bcabe8fcd2c829
signature Signed by PGP key B5690EEEBB952194

Refine T3 Connect authorization surfaces (#4159)

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

5 files changed, 89 insertions(+), 38 deletions(-)

apps/server/src/cloud/cliAuthHtml.test.ts+21-3
...@@ -1,13 +1,31 @@...@@ -1,13 +1,31 @@
1import { expect, it } from "@effect/vitest";1import { expect, it } from "@effect/vitest";
22
3import { renderLoopbackAuthorizationCompleteHtml } from "./cliAuthHtml.ts";3import {
4 renderLoopbackAuthorizationCompleteHtml,
5 resolveLoopbackAuthorizationStage,
6} from "./cliAuthHtml.ts";
47
5it("renders the branded loopback authorization completion page", () => {8it("renders the branded loopback authorization completion page", () => {
6 const html = renderLoopbackAuthorizationCompleteHtml();9 const html = renderLoopbackAuthorizationCompleteHtml();
710
8 expect(html).toContain("T3 Code");11 expect(resolveLoopbackAuthorizationStage()).toBe("dev");
12 expect(html).toContain("T3 Code (Dev)");
13 expect(html).toContain('class="stage stage-dev"');
9 expect(html).not.toContain("Secure terminal handoff");14 expect(html).not.toContain("Secure terminal handoff");
10 expect(html).toContain("You're connected");15 expect(html).toContain("You're connected");
11 expect(html).toContain("return to the terminal");16 expect(html).toContain("Return to your terminal");
17 expect(html).not.toContain('class="next"');
12 expect(html).toContain('name="viewport"');18 expect(html).toContain('name="viewport"');
19 expect(html).not.toContain('class="status"');
20});
21
22it("renders the matching header treatment for each release channel", () => {
23 const nightly = renderLoopbackAuthorizationCompleteHtml("nightly");
24 const latest = renderLoopbackAuthorizationCompleteHtml("latest");
25
26 expect(nightly).toContain("T3 Code (Nightly)");
27 expect(nightly).toContain('class="stage stage-nightly"');
28 expect(latest).toContain('<p class="brand">T3 Code</p>');
29 expect(latest).not.toContain("(Latest)");
30 expect(latest).toContain('class="stage stage-latest"');
13});31});
apps/server/src/cloud/cliAuthHtml.ts+50-35
...@@ -1,4 +1,22 @@...@@ -1,4 +1,22 @@
1export function renderLoopbackAuthorizationCompleteHtml(): string {1export type LoopbackAuthorizationStage = "dev" | "nightly" | "latest";
2
3declare const __T3CODE_BUILD_CHANNEL__: "nightly" | "latest" | undefined;
4
5export function resolveLoopbackAuthorizationStage(): LoopbackAuthorizationStage {
6 return typeof __T3CODE_BUILD_CHANNEL__ === "undefined" ? "dev" : __T3CODE_BUILD_CHANNEL__;
7}
8
9const stageBrands = {
10 dev: "T3 Code (Dev)",
11 nightly: "T3 Code (Nightly)",
12 latest: "T3 Code",
13} as const satisfies Record<LoopbackAuthorizationStage, string>;
14
15export function renderLoopbackAuthorizationCompleteHtml(
16 stage: LoopbackAuthorizationStage = resolveLoopbackAuthorizationStage(),
17): string {
18 const stageBrand = stageBrands[stage];
19
2 return `<!doctype html>20 return `<!doctype html>
3<html lang="en">21<html lang="en">
4 <head>22 <head>
...@@ -38,9 +56,16 @@ export function renderLoopbackAuthorizationCompleteHtml(): string {...@@ -38,9 +56,16 @@ export function renderLoopbackAuthorizationCompleteHtml(): string {
38 overflow: hidden;56 overflow: hidden;
39 padding: 22px 24px;57 padding: 22px 24px;
40 color: white;58 color: white;
59 }
60 .stage-latest {
61 background:
62 radial-gradient(circle at 76% 18%, rgba(136, 204, 255, 0.52), transparent 38%),
63 linear-gradient(135deg, #2468df, #172f82);
64 }
65 .stage-dev {
41 background: linear-gradient(145deg, #5ab8fa 0%, #347ff8 46%, #1939bd 100%);66 background: linear-gradient(145deg, #5ab8fa 0%, #347ff8 46%, #1939bd 100%);
42 }67 }
43 .stage::before {68 .stage-dev::before {
44 content: "";69 content: "";
45 position: absolute;70 position: absolute;
46 inset: 0;71 inset: 0;
...@@ -52,6 +77,22 @@ export function renderLoopbackAuthorizationCompleteHtml(): string {...@@ -52,6 +77,22 @@ export function renderLoopbackAuthorizationCompleteHtml(): string {
52 linear-gradient(90deg, rgba(234, 246, 255, 0.12) 1px, transparent 1px);77 linear-gradient(90deg, rgba(234, 246, 255, 0.12) 1px, transparent 1px);
53 background-size: 32px 32px, 32px 32px, 8px 8px, 8px 8px;78 background-size: 32px 32px, 32px 32px, 8px 8px, 8px 8px;
54 }79 }
80 .stage-nightly {
81 background:
82 radial-gradient(22rem 8rem at 78% 18%, rgba(81, 101, 216, 0.42), transparent 58%),
83 linear-gradient(145deg, #07152f 0%, #151443 52%, #32155b 100%);
84 }
85 .stage-nightly::before {
86 content: "";
87 position: absolute;
88 inset: 0;
89 opacity: 0.78;
90 background-image:
91 radial-gradient(circle at 12px 12px, rgba(228, 234, 255, 0.9) 0 1px, transparent 1.5px),
92 radial-gradient(circle at 38px 28px, rgba(228, 234, 255, 0.58) 0 0.8px, transparent 1.3px),
93 radial-gradient(circle at 58px 9px, rgba(200, 215, 255, 0.72) 0 0.9px, transparent 1.4px);
94 background-size: 72px 48px, 96px 64px, 128px 56px;
95 }
55 .stage::after {96 .stage::after {
56 content: "";97 content: "";
57 position: absolute;98 position: absolute;
...@@ -63,6 +104,10 @@ export function renderLoopbackAuthorizationCompleteHtml(): string {...@@ -63,6 +104,10 @@ export function renderLoopbackAuthorizationCompleteHtml(): string {
63 .stage-content {104 .stage-content {
64 position: relative;105 position: relative;
65 z-index: 1;106 z-index: 1;
107 height: 100%;
108 display: flex;
109 align-items: center;
110 justify-content: space-between;
66 }111 }
67 .brand {112 .brand {
68 margin: 0;113 margin: 0;
...@@ -73,21 +118,6 @@ export function renderLoopbackAuthorizationCompleteHtml(): string {...@@ -73,21 +118,6 @@ export function renderLoopbackAuthorizationCompleteHtml(): string {
73 }118 }
74 .brand { color: rgba(255, 255, 255, 0.82); }119 .brand { color: rgba(255, 255, 255, 0.82); }
75 .content { padding: 30px 32px 34px; }120 .content { padding: 30px 32px 34px; }
76 .status {
77 width: 42px;
78 height: 42px;
79 display: grid;
80 place-items: center;
81 margin-top: -52px;
82 margin-bottom: 22px;
83 border: 4px solid white;
84 border-radius: 50%;
85 background: #2065df;
86 color: white;
87 font-size: 22px;
88 font-weight: 700;
89 box-shadow: 0 8px 22px rgba(25, 72, 177, 0.3);
90 }
91 .eyebrow {121 .eyebrow {
92 margin: 0 0 8px;122 margin: 0 0 8px;
93 color: #2866cc;123 color: #2866cc;
...@@ -98,41 +128,26 @@ export function renderLoopbackAuthorizationCompleteHtml(): string {...@@ -98,41 +128,26 @@ export function renderLoopbackAuthorizationCompleteHtml(): string {
98 }128 }
99 h1 { margin: 0; font-size: clamp(26px, 5vw, 34px); line-height: 1.12; letter-spacing: -0.035em; }129 h1 { margin: 0; font-size: clamp(26px, 5vw, 34px); line-height: 1.12; letter-spacing: -0.035em; }
100 .description { margin: 12px 0 0; color: #646975; font-size: 15px; line-height: 1.6; }130 .description { margin: 12px 0 0; color: #646975; font-size: 15px; line-height: 1.6; }
101 .next {
102 margin-top: 24px;
103 padding: 14px 16px;
104 border: 1px solid rgba(23, 25, 31, 0.1);
105 border-radius: 12px;
106 background: #f7f8fa;
107 color: #454a55;
108 font-size: 13px;
109 }
110 .next strong { color: #17191f; }
111 @media (prefers-color-scheme: dark) {131 @media (prefers-color-scheme: dark) {
112 :root { background: #101115; color: #f1f3f7; }132 :root { background: #101115; color: #f1f3f7; }
113 body { background: radial-gradient(48rem 22rem at 50% -8rem, rgba(55, 102, 210, 0.2), transparent), #101115; }133 body { background: radial-gradient(48rem 22rem at 50% -8rem, rgba(55, 102, 210, 0.2), transparent), #101115; }
114 main { border-color: rgba(255, 255, 255, 0.1); background: rgba(25, 27, 33, 0.96); }134 main { border-color: rgba(255, 255, 255, 0.1); background: rgba(25, 27, 33, 0.96); }
115 .status { border-color: #191b21; }
116 .eyebrow { color: #77a8ff; }135 .eyebrow { color: #77a8ff; }
117 .description { color: #a8adb8; }136 .description { color: #a8adb8; }
118 .next { border-color: rgba(255, 255, 255, 0.1); background: #20232a; color: #b9bec8; }
119 .next strong { color: #f1f3f7; }
120 }137 }
121 </style>138 </style>
122 </head>139 </head>
123 <body>140 <body>
124 <main>141 <main>
125 <header class="stage">142 <header class="stage stage-${stage}" data-stage="${stage}">
126 <div class="stage-content">143 <div class="stage-content">
127 <p class="brand">T3 Code</p>144 <p class="brand">${stageBrand}</p>
128 </div>145 </div>
129 </header>146 </header>
130 <section class="content">147 <section class="content">
131 <div class="status" aria-hidden="true">✓</div>
132 <p class="eyebrow">Browser authorization complete</p>148 <p class="eyebrow">Browser authorization complete</p>
133 <h1>You're connected</h1>149 <h1>You're connected</h1>
134 <p class="description">The authorization code was delivered securely to your waiting terminal.</p>150 <p class="description">Return to your terminal to finish setting up T3 Connect. You can close this window.</p>
135 <div class="next"><strong>Next:</strong> return to the terminal to finish T3 Connect setup. You can close this window.</div>
136 </section>151 </section>
137 </main>152 </main>
138 </body>153 </body>
apps/server/vite.config.ts+3
...@@ -3,6 +3,7 @@ import { defineConfig, mergeConfig } from "vite-plus";...@@ -3,6 +3,7 @@ import { defineConfig, mergeConfig } from "vite-plus";
33
4import baseConfig from "../../vite.config.ts";4import baseConfig from "../../vite.config.ts";
5import { loadRepoEnv } from "../../scripts/lib/public-config.ts";5import { loadRepoEnv } from "../../scripts/lib/public-config.ts";
6import packageJson from "./package.json" with { type: "json" };
67
7const bundledPackagePrefixes = [8const bundledPackagePrefixes = [
8 "@pierre/diffs",9 "@pierre/diffs",
...@@ -16,6 +17,7 @@ export function shouldBundleCliDependency(id: string): boolean {...@@ -16,6 +17,7 @@ export function shouldBundleCliDependency(id: string): boolean {
16}17}
1718
18const repoEnv = loadRepoEnv();19const repoEnv = loadRepoEnv();
20const cliBuildChannel = packageJson.version.includes("-nightly.") ? "nightly" : "latest";
1921
20export default mergeConfig(22export default mergeConfig(
21 baseConfig,23 baseConfig,
...@@ -42,6 +44,7 @@ export default mergeConfig(...@@ -42,6 +44,7 @@ export default mergeConfig(
42 js: "#!/usr/bin/env node\n",44 js: "#!/usr/bin/env node\n",
43 },45 },
44 define: {46 define: {
47 __T3CODE_BUILD_CHANNEL__: JSON.stringify(cliBuildChannel),
45 __T3CODE_BUILD_RELAY_URL__: JSON.stringify(repoEnv.T3CODE_RELAY_URL?.trim() ?? ""),48 __T3CODE_BUILD_RELAY_URL__: JSON.stringify(repoEnv.T3CODE_RELAY_URL?.trim() ?? ""),
46 __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__: JSON.stringify(49 __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__: JSON.stringify(
47 repoEnv.T3CODE_CLERK_PUBLISHABLE_KEY?.trim() ?? "",50 repoEnv.T3CODE_CLERK_PUBLISHABLE_KEY?.trim() ?? "",
apps/web/src/branding.logic.ts+4
...@@ -4,6 +4,10 @@ export function formatAppDisplayName(input: {...@@ -4,6 +4,10 @@ export function formatAppDisplayName(input: {
4 readonly baseName: string;4 readonly baseName: string;
5 readonly stageLabel: string;5 readonly stageLabel: string;
6}): string {6}): string {
7 if (input.stageLabel.trim().toLowerCase() === "latest") {
8 return input.baseName;
9 }
10
7 return `${input.baseName} (${input.stageLabel})`;11 return `${input.baseName} (${input.stageLabel})`;
8}12}
913
apps/web/src/branding.test.ts+11
...@@ -50,6 +50,17 @@ describe("branding", () => {...@@ -50,6 +50,17 @@ describe("branding", () => {
50 expect(branding.APP_DISPLAY_NAME).toBe("T3 Code (Nightly)");50 expect(branding.APP_DISPLAY_NAME).toBe("T3 Code (Nightly)");
51 });51 });
5252
53 it("does not label the latest hosted app channel", async () => {
54 vi.stubEnv("VITE_HOSTED_APP_CHANNEL", "latest");
55
56 const branding = await import("./branding");
57
58 expect(branding.HOSTED_APP_CHANNEL).toBe("latest");
59 expect(branding.HOSTED_APP_CHANNEL_LABEL).toBe("Latest");
60 expect(branding.APP_STAGE_LABEL).toBe("Latest");
61 expect(branding.APP_DISPLAY_NAME).toBe("T3 Code");
62 });
63
53 it("ignores unknown hosted app channels", async () => {64 it("ignores unknown hosted app channels", async () => {
54 vi.stubEnv("VITE_HOSTED_APP_CHANNEL", "preview");65 vi.stubEnv("VITE_HOSTED_APP_CHANNEL", "preview");
5566