g1t/apps/web/app/routes/invite.tsx

477 lines20,566 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1import { CircleAlert, Lock, Ticket } from "lucide-react";
Merge branch 'worktree-agent-a985c01f43538af01'2import { Form, Link, data, redirect } from "react-router";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look3
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas4import type { InvitePreview, User } from "@g1t/contracts";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look5
6import type { Route } from "./+types/invite";
7import { page } from "../lib/meta";
8import { Mark } from "../components/logo";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas9import { ContinueWithGithub, OrDivider } from "../components/github";
10import { Honeypot } from "../components/honeypot";
Merge branch 'worktree-agent-a985c01f43538af01'11import { Avatar, ButtonLink, ErrorText, Field, Input, SubmitButton } from "../components/ui";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas12import { githubSignInEnabled } from "../lib/github.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look13import { identity } from "../lib/services.server";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas14import { cleanCode, landingFor, looksAutomated, suggestUsername, welcomeCookie } from "../lib/invites";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look15import { clientKey } from "../lib/registration.server";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas16import { assertSameOrigin, getViewer, requireUser, roleIn, startSession } from "../lib/session.server";
17import { rememberWorkspace } from "../lib/workspace-choice";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look18
19export function meta(args: Route.MetaArgs) {
20 return page(args, {
21 title: "You're invited · g1t",
22 description: "An invite to g1t, where people and agents ship software together.",
23 });
24}
25
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas26/**
27 * Where an invite is used, from first click to landing inside: who sent
28 * it and what it is for, then signing up on this page (or signing in, for
29 * an address that has an account), and the workspace or repository it
30 * gives. Signing in or up elsewhere (GitHub, /login) comes back here with
31 * `?accept=1`, which finishes the job.
32 */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look33export async function loader({ request, context, params }: Route.LoaderArgs) {
34 const code = cleanCode(params.code);
35 const viewer = getViewer(context);
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas36 const accepting = new URL(request.url).searchParams.get("accept") === "1";
37 const checked = await identity.checkInvite(code, clientKey(request), { viewer, anyStatus: true });
38 const invite = checked.ok ? checked.value : null;
39 const lands = invite ? landingFor(invite) : null;
40
41 if (viewer && invite) {
42 // Used by this person already: just made the account with it (through
43 // GitHub), or an old link opened again.
44 if (invite.status === "redeemed" && invite.forViewer) throw landIn(request, invite, [], accepting);
45 // Back from signing in to accept an invite sent to their address.
46 if (accepting && invite.status === "pending" && invite.forViewer === true && lands && !alreadyIn(viewer, invite)) {
47 const accepted = await identity.acceptInvite(viewer, code);
48 if (accepted.ok) throw landIn(request, invite, [], true);
49 return { ...base(), acceptError: accepted.error.message };
50 }
51 }
52
53 function base() {
54 return {
55 code,
56 invite,
57 error: checked.ok ? null : checked.error.message,
58 viewer: viewer ? { username: viewer.username, avatar: viewer.avatar ?? null } : null,
59 alreadyIn: viewer && invite ? alreadyIn(viewer, invite) : false,
60 github: false,
61 suggestion: suggestUsername(invite?.address),
62 started: Date.now(),
63 acceptError: null as string | null,
64 };
65 }
66 const signingUp = !viewer && invite?.status === "pending" && invite.kind === "account" && !invite.hasAccount;
67 return { ...base(), github: signingUp ? await githubSignInEnabled() : false };
68}
69
70/** Whether the viewer is in what the invite gives already. */
71function alreadyIn(viewer: User, invite: InvitePreview): boolean {
72 if (invite.workspace) return roleIn(viewer, invite.workspace.slug) !== null;
73 return false;
74}
75
76/**
77 * The redirect into what the invite gave, with the session (when one was
78 * just made), the welcome for that first view, and the workspace made the
79 * one the sidebar is about.
80 */
81function landIn(request: Request, invite: InvitePreview, cookies: string[], welcome: boolean): Response {
82 const secure = new URL(request.url).protocol === "https:";
83 const target = landingFor(invite);
84 const headers = new Headers();
85 for (const cookie of cookies) headers.append("set-cookie", cookie);
86 if (target && welcome) headers.append("set-cookie", welcomeCookie(target, secure));
87 if (invite.workspace) headers.append("set-cookie", rememberWorkspace(invite.workspace.slug, secure));
88 return redirect(target ? `/${target}` : "/", { headers });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look89}
90
91export async function action({ request, context, params }: Route.ActionArgs) {
92 assertSameOrigin(request);
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas93 const code = cleanCode(params.code);
94 const form = await request.formData();
95 const client = clientKey(request);
96 const checked = await identity.checkInvite(code, client, { viewer: getViewer(context) });
97 if (!checked.ok) return data({ error: checked.error.message }, { status: 422 });
98 const invite = checked.value;
99
100 if (form.get("intent") === "register") {
101 if (looksAutomated(form)) return data({ error: "Something went wrong. Try again." }, { status: 422 });
102 // An invite for one address makes the account with that address,
103 // whatever the form sent.
104 const email = invite.address ?? String(form.get("email") ?? "");
105 const result = await identity.register(
106 String(form.get("username") ?? ""),
107 email,
108 String(form.get("password") ?? ""),
109 code,
110 client,
111 );
112 if (!result.ok) return data({ error: result.error.message }, { status: 422 });
113 throw landIn(request, invite, [startSession(result.value.sessionToken)], true);
114 }
115
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look116 const user = requireUser(context, request);
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas117 const result = await identity.acceptInvite(user, code);
Merge branch 'worktree-agent-a985c01f43538af01'118 // Said with a 200, so the page loads again and shows the invite as it
119 // now stands (used up, revoked) beside the reason: after a 4xx answer
120 // React Router keeps the page's data as it was.
121 if (!result.ok) return { error: result.error.message };
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas122 throw landIn(request, invite, [], true);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look123}
124
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas125/** Who sent it, and the workspace it joins, as faces. */
126function Faces({ invite }: { invite: InvitePreview }) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look127 const from = invite.invitedBy;
128 return (
129 <div className="flex items-center gap-3">
130 {from ? (
131 <Avatar name={from.username} image={from.avatar} size={44} />
132 ) : (
133 <span className="inline-flex size-11 shrink-0 items-center justify-center rounded-full bg-accent/15 text-accent">
134 <Ticket size={20} />
135 </span>
136 )}
137 {invite.workspace && (
138 <>
139 <span className="text-faint">→</span>
140 <Avatar name={invite.workspace.slug} image={invite.workspace.avatar} size={44} square />
141 </>
142 )}
143 </div>
144 );
145}
146
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas147function senderName(invite: InvitePreview): string {
148 return invite.invitedBy ? (invite.invitedBy.name ?? invite.invitedBy.username) : "The g1t team";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look149}
150
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas151/** "Chase Pierce invited you to join Flagon, Inc. on g1t", with the place in bold. */
152function Headline({ invite }: { invite: InvitePreview }) {
153 const from = senderName(invite);
154 if (invite.workspace) {
155 return (
156 <>
157 {from} invited you to join <strong className="font-semibold text-fg">{invite.workspace.name}</strong> on g1t
158 </>
159 );
160 }
161 if (invite.repository) {
162 return (
163 <>
164 {from} invited you to collaborate on <strong className="font-mono font-semibold text-fg">{invite.repository.name}</strong>
165 </>
166 );
167 }
168 return <>{from} invited you to g1t</>;
169}
170
171function about(invite: InvitePreview, signedIn: boolean): string {
172 const signingUp = !signedIn && !invite.hasAccount && invite.kind === "account";
173 if (invite.workspace) {
174 return `g1t is where people and agents ship software together. ${
175 signingUp ? "Make your account below and you join" : "Accepting joins you to"
176 } ${invite.workspace.name} as a member.`;
177 }
178 if (invite.repository) {
179 return `g1t is where people and agents ship software together. ${
180 signingUp ? "Make your account below and you get" : "Accepting gives you"
181 } the ${invite.repository.role} role on ${invite.repository.name}.`;
182 }
183 return "g1t is where people and agents ship software together: plan in issues, assign work to agents like teammates, and land it through checks that hold. It is invite-only for now; this invite gets you in.";
184}
185
186/** What accepting is called on its button. */
187function joinLabel(invite: InvitePreview): string {
188 if (invite.workspace) return `Join ${invite.workspace.name}`;
189 if (invite.repository) return `Accept access to ${invite.repository.name}`;
190 return "Accept invite";
191}
192
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look193const LONG_DATE = new Intl.DateTimeFormat("en-US", { month: "long", day: "numeric", year: "numeric", timeZone: "UTC" });
194
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas195type Loaded = Route.ComponentProps["loaderData"];
196
197/** Signing up, on this page: the address the invite was sent to, a username, a password. */
198function SignUp({ loaded, error }: { loaded: Loaded; error: string | null }) {
199 const invite = loaded.invite!;
200 const here = `/invite/${loaded.code}`;
201 const back = `${here}?accept=1`;
202 const github = `/auth/github?${new URLSearchParams({ invite: loaded.code, next: back })}`;
203 return (
204 <section aria-labelledby="sign-up" className="rounded-xl border border-line bg-surface/60 p-5 sm:p-6">
205 <h2 id="sign-up" className="text-base font-semibold">
206 Create your account
207 </h2>
208 <p className="mt-1 text-sm text-muted">
209 {invite.workspace
210 ? `You land in ${invite.workspace.name} as soon as it is made.`
211 : invite.repository
212 ? `You land in ${invite.repository.name} as soon as it is made.`
213 : "It takes a minute."}
214 </p>
215 {loaded.github && (
216 <div className="mt-5">
217 <ContinueWithGithub href={github} />
218 <OrDivider />
219 </div>
220 )}
221 <Form method="post" className={`relative space-y-4 ${loaded.github ? "" : "mt-5"}`}>
222 <input type="hidden" name="intent" value="register" />
223 <Honeypot started={loaded.started} />
224 {invite.address ? (
225 <Field label="Email" hint="Your invite was sent here, so this address is confirmed already.">
226 <span className="relative block">
227 <Input name="email" type="email" value={invite.address} readOnly aria-readonly="true" autoComplete="email" />
228 <Lock size={14} aria-hidden="true" className="pointer-events-none absolute top-1/2 right-3 -translate-y-1/2 text-faint" />
229 </span>
230 </Field>
231 ) : (
232 <Field label="Email" hint="We send a link to confirm it.">
233 <Input name="email" type="email" autoComplete="email" required maxLength={254} />
234 </Field>
235 )}
236 <Field label="Username" hint="Lowercase letters, digits and hyphens. It is how you sign in and how others see you.">
237 <Input
238 name="username"
239 autoComplete="username"
240 required
241 autoFocus
242 maxLength={39}
243 defaultValue={loaded.suggestion}
244 pattern="[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9]))*"
245 />
246 </Field>
247 <Field label="Password" hint="At least 10 characters.">
248 <Input name="password" type="password" autoComplete="new-password" required minLength={10} />
249 </Field>
250 <ErrorText>{error}</ErrorText>
251 <div className="pt-1 *:w-full">
Merge branch 'worktree-agent-a985c01f43538af01'252 <SubmitButton pending="Creating account…" match={{ intent: "register" }}>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas253 {invite.workspace
254 ? `Create account and join ${invite.workspace.name}`
255 : invite.repository
256 ? "Create account and accept"
257 : "Create account"}
Merge branch 'worktree-agent-a985c01f43538af01'258 </SubmitButton>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas259 </div>
260 </Form>
261 <p className="mt-5 text-center text-sm text-muted">
262 Already on g1t?{" "}
263 <Link to={`/login?next=${encodeURIComponent(back)}`} className="text-fg underline underline-offset-4">
264 Sign in to accept
265 </Link>
266 </p>
267 </section>
268 );
269}
270
271/** What to do next: join, sign in, sign out, or sign up. */
272function Next({ loaded, error }: { loaded: Loaded; error: string | null }) {
273 const invite = loaded.invite!;
274 const viewer = loaded.viewer;
275 const here = `/invite/${loaded.code}`;
276 const back = `${here}?accept=1`;
277 const joinable = invite.workspace !== null || invite.repository !== null;
278
279 if (viewer) {
280 const signedInAs = (
281 <p className="flex items-center gap-2 text-sm text-muted">
282 <Avatar name={viewer.username} image={viewer.avatar} size={20} />
283 Signed in as <span className="font-mono text-fg">{viewer.username}</span>
284 </p>
285 );
Merge branch 'worktree-agent-a985c01f43538af01'286 // Its own intent, which /logout ignores, so only its button says it is working.
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas287 const signOut = (label: string, variant: "primary" | "quiet") => (
288 <Form method="post" action={`/logout?next=${encodeURIComponent(here)}`}>
Merge branch 'worktree-agent-a985c01f43538af01'289 <input type="hidden" name="intent" value="sign-out" />
290 <SubmitButton variant={variant} pending="Signing out…" match={{ intent: "sign-out" }}>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas291 {label}
Merge branch 'worktree-agent-a985c01f43538af01'292 </SubmitButton>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas293 </Form>
294 );
295 if (invite.forViewer === false) {
296 return (
297 <div className="space-y-4">
298 {signedInAs}
299 <div className="rounded-md border border-warn/40 bg-warn/5 p-4 text-sm" role="status">
300 <p className="font-medium text-fg">This invite is for {invite.address ?? invite.email}, not this account.</p>
301 <p className="mt-1 text-muted">
302 Only an account with that address confirmed can use it. Sign out, then sign in or sign up with it.
303 </p>
304 </div>
305 <ErrorText>{error}</ErrorText>
306 <div className="flex flex-wrap gap-3">{signOut("Sign out and continue", "primary")}</div>
307 </div>
308 );
309 }
310 if (loaded.alreadyIn && invite.workspace) {
311 return (
312 <div className="space-y-4">
313 {signedInAs}
314 <p className="text-sm text-muted">You are in {invite.workspace.name} already.</p>
315 <ButtonLink to={`/${invite.workspace.slug}`}>Go to {invite.workspace.name}</ButtonLink>
316 </div>
317 );
318 }
319 if (!joinable) {
320 return (
321 <div className="space-y-4">
322 {signedInAs}
323 <div className="rounded-md border border-line bg-surface p-4 text-sm">
324 <p>
325 You already have a g1t account, <span className="font-mono">{viewer.username}</span>, so this invite has
326 nothing more to give you.
327 </p>
328 <p className="mt-2 text-muted">Pass it on to whoever it was meant for, or keep it for someone else.</p>
329 </div>
330 <div className="flex flex-wrap gap-3">{signOut("Sign out to use it", "quiet")}</div>
331 </div>
332 );
333 }
334 return (
Merge branch 'worktree-agent-a985c01f43538af01'335 <>
336 {/* "Not you?" posts this form, so it does not carry the accept form's intent. */}
337 <Form id="invite-sign-out" method="post" action={`/logout?next=${encodeURIComponent(here)}`} hidden>
338 <input type="hidden" name="intent" value="sign-out" />
339 </Form>
340 <Form method="post" className="space-y-4">
341 <input type="hidden" name="intent" value="accept" />
342 {signedInAs}
343 {invite.forViewer === null && (
344 <p className="text-sm text-muted">This invite is for anyone with the link. Accepting uses it up.</p>
345 )}
346 <ErrorText>{error}</ErrorText>
347 <div className="flex flex-wrap items-center gap-3">
348 <SubmitButton pending={invite.workspace ? "Joining…" : "Accepting…"} match={{ intent: "accept" }}>
349 {joinLabel(invite)}
350 </SubmitButton>
351 <span className="text-sm text-muted">
352 Not you?{" "}
353 <SubmitButton
354 form="invite-sign-out"
355 pending="Signing out…"
356 match={{ intent: "sign-out" }}
357 className="inline-flex items-center gap-1 text-fg underline underline-offset-4 disabled:opacity-50"
358 >
359 Sign out
360 </SubmitButton>
361 </span>
362 </div>
363 </Form>
364 </>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas365 );
366 }
367
368 // Someone already on g1t: sign in, and the invite is accepted on return.
369 if (invite.hasAccount || invite.kind === "workspace") {
370 return (
371 <div className="space-y-4">
372 <div className="rounded-md border border-line bg-surface p-4 text-sm">
373 <p className="font-medium text-fg">{invite.address ?? invite.email ?? "This address"} has a g1t account.</p>
374 <p className="mt-1 text-muted">
375 Sign in to it and {joinable ? `you ${invite.workspace ? `join ${invite.workspace.name}` : `get access to ${invite.repository!.name}`} straight away` : "the invite is accepted"}.
376 </p>
377 </div>
378 <ErrorText>{error}</ErrorText>
379 <ButtonLink to={`/login?next=${encodeURIComponent(back)}`}>Sign in to accept</ButtonLink>
380 </div>
381 );
382 }
383 return <SignUp loaded={loaded} error={error} />;
384}
385
386/** A code that cannot be used: why, and whom to ask. */
387function Dead({ loaded }: { loaded: Loaded }) {
388 const invite = loaded.invite;
389 const title = !invite
390 ? "This invite link does not work"
391 : invite.status === "expired"
392 ? "This invite has expired"
393 : invite.status === "revoked"
394 ? "This invite was withdrawn"
395 : "This invite has been used";
396 const from = invite?.invitedBy;
397 return (
398 <>
399 <h1 className="mt-6 flex items-center gap-2 text-2xl font-semibold tracking-tight">
400 <CircleAlert size={22} className="shrink-0 text-warn" />
401 {title}
402 </h1>
403 {invite ? (
404 <p className="mt-3 text-sm leading-6 text-muted">
405 It was {invite.workspace ? `an invite to join ${invite.workspace.name}` : invite.repository ? `an invite to ${invite.repository.name}` : "an invite to g1t"}
406 {invite.status === "expired" && `, and stopped working on ${LONG_DATE.format(new Date(invite.expiresAt))}`}.{" "}
407 {from ? "Ask the person who sent it for a new one." : "Ask for access and we will send a new one."}
408 </p>
409 ) : (
410 <p className="mt-3 text-sm leading-6 text-muted">{loaded.error}</p>
411 )}
412 {from && (
413 <div className="mt-5 flex items-center gap-3 rounded-lg border border-line bg-surface p-3">
414 <Avatar name={from.username} image={from.avatar} size={36} />
415 <p className="min-w-0 text-sm">
416 Ask <span className="font-medium text-fg">{from.name ?? from.username}</span>{" "}
417 <Link to={`/u/${from.username}`} className="font-mono text-muted hover:text-fg">
418 @{from.username}
419 </Link>{" "}
420 <span className="text-muted">for a new invite.</span>
421 </p>
422 </div>
423 )}
424 <div className="mt-8 flex flex-wrap gap-3">
425 <ButtonLink to="/register#request">Request access</ButtonLink>
426 <ButtonLink to={loaded.viewer ? "/" : "/login"} variant="quiet">
427 {loaded.viewer ? "Go to g1t" : "Sign in"}
428 </ButtonLink>
429 </div>
430 </>
431 );
432}
433
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look434export default function Invite({ loaderData, actionData }: Route.ComponentProps) {
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas435 const { invite } = loaderData;
436 const error = (actionData && "error" in actionData ? actionData.error : null) ?? loaderData.acceptError;
437 const usable = invite?.status === "pending";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look438 return (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas439 <main className="mx-auto flex max-w-md flex-col px-4 pt-16 pb-12 sm:pt-20">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look440 <Mark className="size-9" />
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas441 {usable ? (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look442 <>
443 <div className="mt-8">
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas444 <Faces invite={invite} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look445 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas446 <h1 className="mt-6 text-2xl font-semibold tracking-tight text-balance text-fg-soft">
447 <Headline invite={invite} />
448 </h1>
449 <p className="mt-2 text-sm leading-6 text-muted">{about(invite, loaderData.viewer !== null)}</p>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look450 <dl className="mt-5 space-y-1 text-sm">
451 {invite.invitedBy && (
452 <div className="flex gap-2">
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas453 <dt className="w-24 shrink-0 text-faint">From</dt>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look454 <dd className="font-mono text-fg-soft">@{invite.invitedBy.username}</dd>
455 </div>
456 )}
457 {invite.email && (
458 <div className="flex gap-2">
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas459 <dt className="w-24 shrink-0 text-faint">For</dt>
460 <dd className="font-mono break-all text-fg-soft">{invite.address ?? invite.email}</dd>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look461 </div>
462 )}
463 <div className="flex gap-2">
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas464 <dt className="w-24 shrink-0 text-faint">Works until</dt>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look465 <dd className="text-fg-soft">{LONG_DATE.format(new Date(invite.expiresAt))}</dd>
466 </div>
467 </dl>
468 <div className="mt-8">
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas469 <Next loaded={loaderData} error={error} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look470 </div>
471 </>
472 ) : (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas473 <Dead loaded={loaderData} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look474 )}
475 </main>
476 );
477}

This file's history is long; its oldest lines are credited to the oldest commit read.