637 lines27,391 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.

Billing on Stripe's pages, month-end charges, warnings; sudo by workspace1import { ArrowLeft, Building2, LogOut } from "lucide-react";
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales2import type { ReactNode } from "react";
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace3import { data, Link, useLocation } from "react-router";
4
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look5import { type AccountSummary, type AdminAction, type Entitlements, type Limit, type Overage, type Terms, httpStatus } from "@g1t/contracts";
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace6
7import type { Route } from "./+types/workspace";
8import {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put9 AllowancesForm,
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace10 AuditSection,
11 BillingLinkSection,
12 CreditForm,
13 Figure,
14 LedgerSection,
15 Owners,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look16 PaymentForm,
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace17 ReviewPanel,
18 TermsForm,
19} from "~/components/billing";
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales20import { MonthsChart } from "~/components/charts";
21import { SalesSection, StageBadge, WorkspaceInvoicesSection } from "~/components/sales";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look22import {
23 Avatar,
24 Badge,
25 Button,
26 EmptyState,
27 ExposureBar,
28 Field,
29 Input,
30 Notice,
31 Section,
32 Select,
33 StateBadge,
34 TermsBadge,
35 Textarea,
36 TrustBadge,
37 When,
38 trustAbout,
39} from "~/components/ui";
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace40import { type Subject, billingAction } from "~/lib/billing-actions.server";
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales41import { parseSlug, text } from "~/lib/forms";
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace42import { usd } from "~/lib/money";
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales43import { type ActionData, DONE, type SectionError, doneKey } from "~/lib/review";
44import { SALES_INTENTS, salesAction } from "~/lib/sales-actions.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look45import { goodwillWarning, spikeLabel } from "~/lib/pricing";
46import { admin, entitlements as entitlementsOf, identity, priceBook } from "~/lib/services.server";
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales47import { settle } from "~/lib/settle";
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace48import { requireStaff } from "~/lib/staff";
49import type { Enterprise } from "~/lib/workspaces";
50
51export const meta: Route.MetaFunction = ({ loaderData }) => [
52 { title: `${loaderData?.name ?? "Workspace"} · sudo` },
53 { name: "robots", content: "noindex, nofollow" },
54];
55
56/** Whether an audit line is about `slug`, in an enterprise's log. */
57function mentions(action: AdminAction, slug: string): boolean {
58 // A slug is letters, digits and hyphens, none special in a pattern.
59 return new RegExp(`(^|[^a-z0-9-])${slug}($|[^a-z0-9-])`).test(action.detail.toLowerCase());
60}
61
62async function load(raw: string) {
63 const parsed = parseSlug(raw);
64 if (!parsed.ok) throw data("That is not a workspace slug.", { status: 404 });
65 const slug = parsed.value;
66 const [person, result] = await Promise.all([identity.workspace(slug), admin.account(slug)]);
67 if (!result.ok) throw data(result.error.message, { status: httpStatus(result.error) });
68 const detail = result.value;
69 const { account } = detail.summary;
70 const billedTo: Enterprise | null = account.kind === "enterprise" ? { id: account.id, name: account.name } : null;
71 const known = person != null || billedTo != null || account.createdAt !== "" || detail.ledger.length > 0 || detail.audit.length > 0;
72 if (!known) throw data(`There is no workspace called ${slug}.`, { status: 404 });
73 const subject: Subject = { kind: "workspace", slug, accountId: account.id, terms: account.terms, billedTo };
74 return { slug, person, detail, billedTo, subject };
75}
76
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales77/** Sales and invoices are newer billing methods; a page still opens without them. */
78const SALES_DONE = new Set(["sales", "note"]);
79
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace80export async function loader({ request, params, context }: Route.LoaderArgs) {
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales81 const staff = requireStaff(context);
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace82 const { slug, person, detail, billedTo, subject } = await load(params.slug);
83 const { summary } = detail;
84 // On an enterprise, the limit is the enterprise's and the figures are
85 // this workspace's share of its bill.
86 const limit = (billedTo && detail.workspaces.find((member) => member.workspace === slug)) || summary.limit;
87 const share = billedTo ? summary.byWorkspace?.find((row) => row.workspace === slug) : null;
88 const figures = billedTo
89 ? { charged: share?.chargedMicros ?? 0, cost: share?.costMicros ?? 0, paid: share?.paidMicros ?? 0 }
90 : { charged: summary.chargedMicros, cost: summary.costMicros, paid: summary.paidMicros };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look91 const [enterprises, sales, invoices, plan, overages, book] = await Promise.all([
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales92 billedTo
93 ? Promise.resolve([])
94 : admin
95 .accounts()
96 .then((rows: AccountSummary[]) =>
97 rows
98 .filter((row) => row.account.kind === "enterprise")
99 .map((row) => ({ id: row.account.id, name: row.account.name, members: row.account.workspaces.length })),
100 ),
101 settle(admin.sales(slug)),
102 settle(admin.workspaceInvoices(slug)),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look103 settle(entitlementsOf(slug)),
104 settle(admin.overages()),
105 settle(priceBook()),
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales106 ]);
107 const done = doneKey(request.url);
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace108 return {
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales109 me: staff.email,
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace110 slug,
111 name: person?.name ?? slug,
112 person,
113 billedTo,
114 terms: subject.terms,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put115 allowances: summary.account.allowances,
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace116 limit,
117 figures,
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales118 months: summary.months ?? [],
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace119 enterprises,
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales120 sales: sales.ok ? sales.value : null,
121 salesError: sales.ok ? null : sales.error,
122 invoices: invoices.ok ? invoices.value : [],
123 invoicesError: invoices.ok ? null : invoices.error,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look124 entitlements: plan.ok ? plan.value : null,
125 entitlementsError: plan.ok ? null : plan.error,
126 overage: overages.ok ? (overages.value.find((row) => row.workspace === slug) ?? null) : null,
127 forgiveCap: (book.ok && book.value.free?.overageForgiveCostMicros) || 50_000_000,
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace128 ledger: billedTo ? detail.ledger.filter((entry) => !entry.workspace || entry.workspace === slug) : detail.ledger,
129 audit: billedTo ? detail.audit.filter((entry) => mentions(entry, slug)) : detail.audit,
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales130 done: done && !SALES_DONE.has(done) ? DONE[done] : null,
131 salesDone: done && SALES_DONE.has(done) ? DONE[done] : null,
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace132 };
133}
134
135export async function action({ request, params, context }: Route.ActionArgs) {
136 const staff = requireStaff(context);
137 // What is acted on comes from billing and identity, not from the form.
138 const { slug, subject } = await load(params.slug);
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales139 const path = `/workspaces/${encodeURIComponent(slug)}`;
140 const form = await request.clone().formData();
141 if (SALES_INTENTS.has(text(form, "intent"))) return salesAction(form, staff, slug, path);
142 return billingAction(request, staff, subject, path);
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace143}
144
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales145const SECTIONS = [
146 { id: "members", label: "Members" },
147 { id: "sales", label: "Sales" },
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look148 { id: "plan", label: "Plan" },
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales149 { id: "billing", label: "Billing" },
150 { id: "invoices", label: "Invoices" },
151 { id: "ledger", label: "Ledger" },
152 { id: "audit", label: "Audit log" },
153];
154
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace155export default function Workspace({ loaderData, actionData }: Route.ComponentProps) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look156 const {
157 me,
158 slug,
159 name,
160 person,
161 billedTo,
162 terms,
163 allowances,
164 limit,
165 figures,
166 months,
167 enterprises,
168 sales,
169 salesError,
170 invoices,
171 invoicesError,
172 entitlements,
173 entitlementsError,
174 overage,
175 forgiveCap,
176 ledger,
177 audit,
178 done,
179 salesDone,
180 } = loaderData;
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace181 const { pathname } = useLocation();
182 const result = actionData as ActionData | undefined;
183 const review = result && "review" in result ? result.review : null;
184 const link = result && "link" in result ? result.link : null;
185 const error = (section: string): SectionError => (result && "error" in result && result.section === section ? result : null);
186 const owners = person?.members.filter((member) => member.role === "owner") ?? [];
187
188 return (
189 <main id="top" className="mx-auto max-w-6xl scroll-mt-20 px-4 py-8 sm:py-10">
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales190 <Link to="/workspaces" className="inline-flex items-center gap-1.5 text-sm text-muted hover:text-fg">
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace191 <ArrowLeft size={14} />
192 Workspaces
193 </Link>
194
195 {/* Header */}
196 <div className="mt-4 flex flex-wrap items-start justify-between gap-4">
197 <div className="flex min-w-0 items-start gap-3">
198 <Avatar name={slug} size={40} />
199 <div className="min-w-0">
200 <h1 className="truncate text-2xl font-semibold tracking-tight">{name}</h1>
201 <p className="mt-0.5 text-xs text-faint">
202 <span className="font-mono">{slug}</span>
203 {person && (
204 <>
205 {" · "}created <When at={person.createdAt} />
206 </>
207 )}
208 </p>
209 {person?.description && <p className="mt-1 text-sm text-muted">{person.description}</p>}
210 <div className="mt-2 flex flex-wrap gap-1.5">
211 {billedTo && <Badge tone="lavender">Billed to {billedTo.name}</Badge>}
212 <TermsBadge terms={terms} />
213 {terms.kind !== "comped" && <TrustBadge trust={limit.trust} />}
214 <StateBadge state={limit.state} />
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales215 {sales && sales.stage !== "none" && <StageBadge stage={sales.stage} />}
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace216 </div>
217 </div>
218 </div>
219 {owners.length > 0 && (
220 <div className="text-xs">
221 <p className="mb-1.5 text-faint">Owners</p>
222 <Owners owners={owners} />
223 </div>
224 )}
225 </div>
226
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales227 <nav aria-label="On this page" className="mt-6 -mx-4 overflow-x-auto px-4">
228 <ul className="flex gap-1 border-b border-line text-sm">
229 {SECTIONS.map((section) => (
230 <li key={section.id}>
231 <a href={`#${section.id}`} className="block border-b-2 border-transparent px-2.5 py-2 whitespace-nowrap text-muted transition-colors hover:border-line-strong hover:text-fg">
232 {section.label}
233 </a>
234 </li>
235 ))}
236 </ul>
237 </nav>
238
239 <div className="mt-6 space-y-3 empty:hidden">
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace240 {done && <Notice tone="ok">{done}</Notice>}
241 {error("top") && <Notice tone="error">{error("top")?.error}</Notice>}
242 {limit.message && <Notice tone={limit.state === "stopped" ? "error" : limit.state === "warning" ? "warn" : "info"}>{limit.message}</Notice>}
243 {review && <ReviewPanel review={review} pathname={pathname} />}
244 </div>
245
246 {/* People */}
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales247 <Section id="members" title="Members" description="Everyone in the workspace. Owners manage its members and billing." className="mt-6">
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace248 {!person ? (
249 <Notice tone="warn">Identity has no record of {slug}; only billing knows it. It may have been deleted.</Notice>
250 ) : person.members.length === 0 ? (
251 <EmptyState title="No members" />
252 ) : (
253 <div className="-mx-4 -my-4 overflow-x-auto sm:-mx-5 sm:-my-5">
254 <table className="w-full min-w-120 text-sm">
255 <thead>
256 <tr className="border-b border-line text-left text-xs text-muted">
257 <th className="px-4 py-2 font-medium sm:pl-5">Member</th>
258 <th className="px-4 py-2 font-medium">Email</th>
259 <th className="px-4 py-2 font-medium">Role</th>
260 <th className="px-4 py-2 font-medium sm:pr-5">Joined</th>
261 </tr>
262 </thead>
263 <tbody>
264 {person.members.map((member) => (
265 <tr key={member.username} className="border-b border-line last:border-0">
266 <td className="px-4 py-2.5 sm:pl-5">
267 <span className="inline-flex items-center gap-2 font-mono">
268 <Avatar name={member.username} size={18} square={false} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look269 <Link to={`/users/${member.username}`} className="hover:underline">
270 {member.username}
271 </Link>
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace272 </span>
273 </td>
274 <td className="px-4 py-2.5 break-all text-muted">{member.email ?? <span className="text-faint">—</span>}</td>
275 <td className="px-4 py-2.5">{member.role === "owner" ? <Badge tone="lavender">Owner</Badge> : <Badge>Member</Badge>}</td>
276 <td className="px-4 py-2.5 text-xs whitespace-nowrap text-muted sm:pr-5">
277 <When at={member.joined} />
278 </td>
279 </tr>
280 ))}
281 </tbody>
282 </table>
283 </div>
284 )}
285 </Section>
286
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales287 {/* Sales */}
288 <div className="mt-6">
289 <SalesSection
290 sales={sales}
291 unavailable={salesError}
292 me={me}
293 pathname={pathname}
294 salesError={error("sales")}
295 noteError={error("note")}
296 done={salesDone}
297 />
298 </div>
299
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look300 {/* Plan */}
301 <div className="mt-6">
302 <PlanSection entitlements={entitlements} unavailable={entitlementsError} />
303 </div>
304
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace305 {/* Billing */}
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales306 <h2 id="billing" className="mt-10 scroll-mt-20 text-lg font-semibold tracking-tight">
307 Billing
308 </h2>
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace309 <div className="mt-3 grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales310 <LimitCard limit={limit} terms={terms} billedTo={billedTo} />
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace311 <Figure label="Charged this month" value={usd(figures.charged)} hint={`Cost to g1t ${usd(figures.cost)}`} />
312 <Figure label="Paid ever" value={usd(figures.paid)} hint={`Margin this month ${usd(figures.charged - figures.cost)}`} />
313 </div>
314
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales315 <Section
316 title="Last six months"
317 description={billedTo ? `${billedTo.name}'s figures: every workspace it pays for, together.` : "What it was charged, against what its usage cost g1t."}
318 className="mt-6"
319 >
320 <MonthsChart wide months={months} label={`${billedTo ? billedTo.name : name}: charged and cost by month`} />
321 </Section>
322
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace323 <div className="mt-6 grid gap-6 lg:grid-cols-[minmax(0,3fr)_minmax(0,2fr)]">
324 <div className="space-y-6">
325 <BilledToSection billedTo={billedTo} enterprises={enterprises} pathname={pathname} error={error("billed-to")} />
326 {billedTo ? (
327 <Section id="terms" title="Terms" description={`Charged on ${billedTo.name}'s terms while it pays for this workspace.`}>
328 <div className="flex flex-wrap items-center justify-between gap-3">
329 <TermsBadge terms={terms} />
330 <Link to={`/enterprises/${encodeURIComponent(billedTo.id)}#terms`} className="text-sm text-merged hover:underline hover:underline-offset-4">
331 Change them on {billedTo.name}
332 </Link>
333 </div>
334 </Section>
335 ) : (
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put336 <>
337 <TermsForm terms={terms} pathname={pathname} error={error("terms")} />
338 <AllowancesForm allowances={allowances} comped={terms.kind === "comped"} pathname={pathname} error={error("allowances")} />
339 </>
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace340 )}
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales341 <WorkspaceInvoicesSection invoices={invoices} unavailable={invoicesError} />
342 <div id="ledger" className="scroll-mt-20">
343 <LedgerSection
344 ledger={ledger}
345 description={billedTo ? `This workspace's lines among ${billedTo.name}'s latest 100.` : "Recent lines of the statement, newest first."}
346 />
347 </div>
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace348 </div>
349
350 <div className="space-y-6">
351 <BillingLinkSection link={link} pathname={pathname} error={error("billing-link")} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look352 {!billedTo && terms.kind !== "comped" && (
353 <GoodwillForm overage={overage} cap={forgiveCap} pathname={pathname} error={error("goodwill")} />
354 )}
355 <PaymentForm workspace={slug} pathname={pathname} error={error("payment")} />
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace356 <CreditForm workspaces={[slug]} pathname={pathname} error={error("credit")} />
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales357 <div id="audit" className="scroll-mt-20">
358 <AuditSection
359 audit={audit}
360 description={
361 billedTo ? (
362 <>
363 Changes about this workspace in {billedTo.name}'s log.{" "}
364 <Link to={`/enterprises/${encodeURIComponent(billedTo.id)}`} className="text-merged hover:underline">
365 Full log
366 </Link>
367 </>
368 ) : undefined
369 }
370 />
371 </div>
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace372 </div>
373 </div>
374 </main>
375 );
376}
377
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales378/**
379 * The limit, and in words how it is made: trust, the owners' own spend
380 * limit (or the default), the most they may set, and how it grows.
381 */
382function LimitCard({ limit, terms, billedTo }: { limit: Limit; terms: Terms; billedTo: Enterprise | null }) {
383 const lines: { label: string; value: ReactNode }[] = [];
384 if (billedTo) {
385 lines.push({ label: "Limit", value: <>{billedTo.name}'s, shared by every workspace it pays for</> });
386 } else if (terms.kind !== "comped") {
387 lines.push({ label: "From trust", value: <>{usd(limit.trustCeilingMicros)}. {trustAbout(limit.trust)}</> });
388 }
389 if (terms.ceilingMicros != null) lines.push({ label: "Custom limit", value: usd(terms.ceilingMicros) });
390 if (!billedTo && terms.kind !== "comped") {
391 lines.push({
392 label: "Owners' limit",
393 value: limit.defaultSpendLimit ? (
394 <>The default, {usd(limit.spendLimitMicros ?? 200_000_000)} a month: they have not chosen one</>
395 ) : limit.spendLimitMicros == null ? (
396 "None of their own: they use everything available"
397 ) : (
398 <>{usd(limit.spendLimitMicros)} a month, chosen by the owners</>
399 ),
400 });
401 if (limit.availableMicros !== undefined) {
402 lines.push({
403 label: "Available",
404 value:
405 limit.availableMicros == null ? (
406 "No ceiling"
407 ) : (
408 <>Up to {usd(limit.availableMicros)}: the most owners may set; past it, they contact g1t</>
409 ),
410 });
411 }
412 }
413 if (limit.growth) lines.push({ label: "Grows", value: limit.growth });
414
415 return (
416 <div className="rounded-lg border border-line bg-surface px-4 py-3 sm:col-span-2">
417 <p className="mb-2 text-xs text-muted">
418 Usage this month against the limit{billedTo && <> · shared with every workspace {billedTo.name} pays for</>}
419 </p>
420 <ExposureBar limit={limit} wide />
421 {lines.length > 0 && (
422 <dl className="mt-3 space-y-1 text-xs">
423 {lines.map((line) => (
424 <div key={line.label} className="grid grid-cols-[6.5rem_minmax(0,1fr)] gap-2">
425 <dt className="text-faint">{line.label}</dt>
426 <dd className="text-fg-soft">{line.value}</dd>
427 </div>
428 ))}
429 </dl>
430 )}
431 </div>
432 );
433}
434
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace435function BilledToSection({
436 billedTo,
437 enterprises,
438 pathname,
439 error,
440}: {
441 billedTo: Enterprise | null;
442 enterprises: { id: string; name: string; members: number }[];
443 pathname: string;
444 error: SectionError;
445}) {
446 return (
447 <Section id="billed-to" title="Billed to" description="Whether the workspace pays for itself, or an enterprise pays for it.">
448 {error && (
449 <div className="mb-4">
450 <Notice tone="error">{error.error}</Notice>
451 </div>
452 )}
453 {billedTo ? (
454 <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
455 <p className="text-sm text-muted">
456 <Link to={`/enterprises/${encodeURIComponent(billedTo.id)}`} className="font-medium text-fg hover:underline hover:underline-offset-4">
457 {billedTo.name}
458 </Link>{" "}
459 pays for it, under its limit and terms.
460 </p>
461 <form method="post" action={`${pathname}#review`}>
462 <input type="hidden" name="intent" value="detach" />
463 <Button type="submit" variant="danger">
464 <LogOut size={14} />
465 Move off
466 </Button>
467 </form>
468 </div>
469 ) : enterprises.length === 0 ? (
470 <p className="text-sm text-muted">
471 Itself. There are no enterprises to move it onto yet;{" "}
472 <Link to="/enterprises/new" className="text-merged hover:underline hover:underline-offset-4">
473 create one
474 </Link>
475 .
476 </p>
477 ) : (
478 <form method="post" action={`${pathname}#review`} className="flex flex-col gap-2 sm:flex-row sm:items-end">
479 <input type="hidden" name="intent" value="attach" />
480 <Field label="Itself. Move onto" className="grow">
481 <Select name="target" required defaultValue={error?.values?.target ?? ""}>
482 <option value="" disabled>
483 Choose an enterprise
484 </option>
485 {enterprises.map((enterprise) => (
486 <option key={enterprise.id} value={enterprise.id}>
487 {enterprise.name} ({enterprise.members} workspace{enterprise.members === 1 ? "" : "s"})
488 </option>
489 ))}
490 </Select>
491 </Field>
492 <Button type="submit" variant="quiet">
493 <Building2 size={14} />
494 Move
495 </Button>
496 </form>
497 )}
498 </Section>
499 );
500}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look501
502const PLAN_LABEL: Record<string, string> = { free: "Free", paid: "The g1t plan", internal: "Internal (comped)", enterprise: "Enterprise" };
503
504function gigabytes(bytes: number): string {
505 return `${Math.round((bytes / 1e9) * 100) / 100} GB`;
506}
507
508/** What billing tells every service about the workspace now: plan, caps, pause, trial and what the plan gives it. */
509function PlanSection({ entitlements: e, unavailable }: { entitlements: Entitlements | null; unavailable: string | null }) {
510 if (!e) {
511 return (
512 <Section id="plan" title="Plan and entitlements">
513 <Notice tone="warn">Billing did not answer for entitlements{unavailable ? `: ${unavailable}` : "."}</Notice>
514 </Section>
515 );
516 }
517 const spike = e.spike ? spikeLabel(e.spike.status) : null;
518 const facts: [string, string][] = [
519 ["Compute", e.compute ? "Yes" : e.plan === "free" && e.trialVerified && e.trialMicrosLeft > 0 ? "On the trial" : "No"],
520 ["Card check", e.trialVerified ? "Done" : "Not yet"],
521 ["Trial left", usd(e.trialMicrosLeft)],
522 ["First month", e.firstMonth ? "Yes" : "No"],
523 ["Agents at once", String(e.maxConcurrentAgents)],
524 ["Longest run", e.maxRunMinutes ? `${e.maxRunMinutes} min` : "The guardrails'"],
525 ["Run cap", usd(e.runCapMicros)],
526 ["Issue cap", usd(e.issueCapMicros)],
527 ["Ceiling", e.ceilingMicros >= 1e15 ? "None" : usd(e.ceilingMicros)],
528 ["Not yet paid", usd(e.exposureMicros)],
529 ["Held by reservations", usd(e.heldMicros ?? 0)],
530 ["Prepaid", usd(e.prepaidMicros ?? 0)],
531 ["Included usage", e.includedMicros ? `${usd(e.includedUsedMicros ?? 0)} of ${usd(e.includedMicros)}` : "—"],
532 ["Private storage", `${gigabytes(e.privateStorageBytes)} of ${gigabytes(e.freePrivateStorageBytes)}`],
533 ["Open-source pool paid", usd(e.ossPaidMicros)],
534 ["Build minutes", `${Math.ceil(e.buildSecondsUsed / 60)} of ${Math.floor(e.buildSecondsIncluded / 60)}`],
535 [
536 "Git operations",
537 e.gitOperations != null
538 ? `${e.gitOperations.toLocaleString("en-US")}${e.gitOperationsIncluded ? ` of ${e.gitOperationsIncluded.toLocaleString("en-US")}` : ""}`
539 : "—",
540 ],
541 ["Audit log", `${e.auditRetentionDays} days`],
542 ];
543 return (
544 <Section
545 id="plan"
546 title="Plan and entitlements"
547 description="What billing tells every service that starts compute for this workspace, now."
548 actions={
549 <span className="flex flex-wrap gap-1.5">
550 <Badge tone={e.plan === "free" ? "plain" : e.plan === "internal" ? "mint" : "lavender"}>{PLAN_LABEL[e.plan] ?? e.plan}</Badge>
551 {spike && <Badge tone={spike.tone === "plain" ? "plain" : spike.tone}>{spike.label}</Badge>}
552 </span>
553 }
554 >
555 {e.paused && (
556 <div className="mb-4">
557 <Notice tone="error">Compute paused: {e.paused}</Notice>
558 </div>
559 )}
560 {(e.alerts ?? []).length > 0 && (
561 <ul className="mb-4 space-y-1 text-sm">
562 {(e.alerts ?? []).map((alert) => (
563 <li key={`${alert.meter}-${alert.level}`} className={alert.level >= 90 ? "text-warn" : "text-muted"}>
564 {alert.level}% · {alert.message || `${alert.meter}: ${usd(alert.usedMicros)} of ${usd(alert.limitMicros)}`}
565 </li>
566 ))}
567 </ul>
568 )}
569 <dl className="grid gap-x-6 gap-y-1 text-xs sm:grid-cols-2 lg:grid-cols-3">
570 {facts.map(([label, value]) => (
571 <div key={label} className="flex justify-between gap-3 border-b border-line/60 py-1">
572 <dt className="text-faint">{label}</dt>
573 <dd className="tabular text-right text-fg-soft">{value}</dd>
574 </div>
575 ))}
576 </dl>
577 <p className="mt-3 text-xs text-faint">Agents at once, the run and issue caps, and holds are set under Plan, pools and caps below.</p>
578 </Section>
579 );
580}
581
582/** A goodwill credit from the workspace's page; Overages shows the same math for the whole queue. */
583function GoodwillForm({ overage, cap, pathname, error }: { overage: Overage | null; cap: number; pathname: string; error: SectionError }) {
584 const values = error?.values;
585 const quote = overage ? goodwillWarning(null, overage.goodwill, overage.lastGoodwillAt, cap) : null;
586 return (
587 <Section id="goodwill" title="Goodwill credit" description="For usage past what the owners meant. One click, once in 12 months; more needs a reason.">
588 {overage ? (
589 <dl className="mb-4 space-y-1 text-sm">
590 <div className="flex justify-between gap-3">
591 <dt className="text-muted">Over the typical month</dt>
592 <dd className="tabular">{usd(overage.goodwill.overageMicros)}</dd>
593 </div>
594 <div className="flex justify-between gap-3">
595 <dt className="text-muted">g1t's margin, returned</dt>
596 <dd className="tabular text-accent">{usd(overage.goodwill.marginMicros)}</dd>
597 </div>
598 <div className="flex justify-between gap-3">
599 <dt className="text-muted">Real cost absorbed</dt>
600 <dd className={`tabular ${quote?.overCap ? "text-danger" : "text-warn"}`}>{usd(overage.goodwill.absorbedMicros)}</dd>
601 </div>
602 <div className="flex justify-between gap-3 border-t border-line pt-1 font-medium">
603 <dt>One-click credit</dt>
604 <dd className="tabular">{usd(overage.goodwill.creditMicros)}</dd>
605 </div>
606 {!overage.goodwillAvailable && (
607 <p className="pt-1 text-xs text-danger">
608 A goodwill credit was given <When at={overage.lastGoodwillAt} />: another needs a reason.
609 </p>
610 )}
611 </dl>
612 ) : (
613 <p className="mb-4 text-sm text-muted">Not in the Overages queue this month: give an amount and a reason.</p>
614 )}
615 <form method="post" action={`${pathname}#goodwill`} className="space-y-3">
616 <input type="hidden" name="intent" value="goodwill" />
617 {error && <Notice tone="error">{error.error}</Notice>}
618 <div className="grid gap-3 sm:grid-cols-2">
619 <Field label="Amount $" hint={overage ? "Blank: the one-click credit." : "Required here."}>
620 <Input name="amount" inputMode="decimal" defaultValue={values?.amount ?? ""} />
621 </Field>
622 <Field label="Day of the usage" hint="Blank: billing chooses.">
623 <Input type="date" name="day" defaultValue={values?.day ?? ""} />
624 </Field>
625 </div>
626 <Field label="Reason" hint={`Needed past the one-click credit. Past ${usd(cap)} of real cost, use Overages to see it in red first.`}>
627 <Textarea name="reason" rows={2} maxLength={500} defaultValue={values?.reason ?? ""} />
628 </Field>
629 <div className="flex justify-end">
630 <Button type="submit" variant="lavender">
631 Give goodwill credit
632 </Button>
633 </div>
634 </form>
635 </Section>
636 );
637}