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 @@
11import { expect, it } from "@effect/vitest";
22
3import { renderLoopbackAuthorizationCompleteHtml } from "./cliAuthHtml.ts";
3import {
4 renderLoopbackAuthorizationCompleteHtml,
5 resolveLoopbackAuthorizationStage,
6} from "./cliAuthHtml.ts";
47
58it("renders the branded loopback authorization completion page", () => {
69 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"');
914 expect(html).not.toContain("Secure terminal handoff");
1015 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"');
1218 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"');
1331});
apps/server/src/cloud/cliAuthHtml.ts+50-35
......@@ -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
220 return `<!doctype html>
321<html lang="en">
422 <head>
......@@ -38,9 +56,16 @@ export function renderLoopbackAuthorizationCompleteHtml(): string {
3856 overflow: hidden;
3957 padding: 22px 24px;
4058 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 {
4166 background: linear-gradient(145deg, #5ab8fa 0%, #347ff8 46%, #1939bd 100%);
4267 }
43 .stage::before {
68 .stage-dev::before {
4469 content: "";
4570 position: absolute;
4671 inset: 0;
......@@ -52,6 +77,22 @@ export function renderLoopbackAuthorizationCompleteHtml(): string {
5277 linear-gradient(90deg, rgba(234, 246, 255, 0.12) 1px, transparent 1px);
5378 background-size: 32px 32px, 32px 32px, 8px 8px, 8px 8px;
5479 }
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 }
5596 .stage::after {
5697 content: "";
5798 position: absolute;
......@@ -63,6 +104,10 @@ export function renderLoopbackAuthorizationCompleteHtml(): string {
63104 .stage-content {
64105 position: relative;
65106 z-index: 1;
107 height: 100%;
108 display: flex;
109 align-items: center;
110 justify-content: space-between;
66111 }
67112 .brand {
68113 margin: 0;
......@@ -73,21 +118,6 @@ export function renderLoopbackAuthorizationCompleteHtml(): string {
73118 }
74119 .brand { color: rgba(255, 255, 255, 0.82); }
75120 .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 }
91121 .eyebrow {
92122 margin: 0 0 8px;
93123 color: #2866cc;
......@@ -98,41 +128,26 @@ export function renderLoopbackAuthorizationCompleteHtml(): string {
98128 }
99129 h1 { margin: 0; font-size: clamp(26px, 5vw, 34px); line-height: 1.12; letter-spacing: -0.035em; }
100130 .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; }
111131 @media (prefers-color-scheme: dark) {
112132 :root { background: #101115; color: #f1f3f7; }
113133 body { background: radial-gradient(48rem 22rem at 50% -8rem, rgba(55, 102, 210, 0.2), transparent), #101115; }
114134 main { border-color: rgba(255, 255, 255, 0.1); background: rgba(25, 27, 33, 0.96); }
115 .status { border-color: #191b21; }
116135 .eyebrow { color: #77a8ff; }
117136 .description { color: #a8adb8; }
118 .next { border-color: rgba(255, 255, 255, 0.1); background: #20232a; color: #b9bec8; }
119 .next strong { color: #f1f3f7; }
120137 }
121138 </style>
122139 </head>
123140 <body>
124141 <main>
125 <header class="stage">
142 <header class="stage stage-${stage}" data-stage="${stage}">
126143 <div class="stage-content">
127 <p class="brand">T3 Code</p>
144 <p class="brand">${stageBrand}</p>
128145 </div>
129146 </header>
130147 <section class="content">
131 <div class="status" aria-hidden="true">✓</div>
132148 <p class="eyebrow">Browser authorization complete</p>
133149 <h1>You're connected</h1>
134 <p class="description">The authorization code was delivered securely to your waiting terminal.</p>
135 <div class="next"><strong>Next:</strong> return to the terminal to finish T3 Connect setup. You can close this window.</div>
150 <p class="description">Return to your terminal to finish setting up T3 Connect. You can close this window.</p>
136151 </section>
137152 </main>
138153 </body>
apps/server/vite.config.ts+3
......@@ -3,6 +3,7 @@ import { defineConfig, mergeConfig } from "vite-plus";
33
44import baseConfig from "../../vite.config.ts";
55import { loadRepoEnv } from "../../scripts/lib/public-config.ts";
6import packageJson from "./package.json" with { type: "json" };
67
78const bundledPackagePrefixes = [
89 "@pierre/diffs",
......@@ -16,6 +17,7 @@ export function shouldBundleCliDependency(id: string): boolean {
1617}
1718
1819const repoEnv = loadRepoEnv();
20const cliBuildChannel = packageJson.version.includes("-nightly.") ? "nightly" : "latest";
1921
2022export default mergeConfig(
2123 baseConfig,
......@@ -42,6 +44,7 @@ export default mergeConfig(
4244 js: "#!/usr/bin/env node\n",
4345 },
4446 define: {
47 __T3CODE_BUILD_CHANNEL__: JSON.stringify(cliBuildChannel),
4548 __T3CODE_BUILD_RELAY_URL__: JSON.stringify(repoEnv.T3CODE_RELAY_URL?.trim() ?? ""),
4649 __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__: JSON.stringify(
4750 repoEnv.T3CODE_CLERK_PUBLISHABLE_KEY?.trim() ?? "",
apps/web/src/branding.logic.ts+4
......@@ -4,6 +4,10 @@ export function formatAppDisplayName(input: {
44 readonly baseName: string;
55 readonly stageLabel: string;
66}): string {
7 if (input.stageLabel.trim().toLowerCase() === "latest") {
8 return input.baseName;
9 }
10
711 return `${input.baseName} (${input.stageLabel})`;
812}
913
apps/web/src/branding.test.ts+11
......@@ -50,6 +50,17 @@ describe("branding", () => {
5050 expect(branding.APP_DISPLAY_NAME).toBe("T3 Code (Nightly)");
5151 });
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
5364 it("ignores unknown hosted app channels", async () => {
5465 vi.stubEnv("VITE_HOSTED_APP_CHANNEL", "preview");
5566