Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
147 linesCodeBlameRaw

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.

Usage, Billing settings and prepaid AI credit; fixes from the UX audit1import { Suspense } from "react";
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens2import { Await, Link, data } from "react-router";
The keeper reads Cloudflare as it really answers3
Usage, Billing settings and prepaid AI credit; fixes from the UX audit4import { PRODUCTS, type UsageReport } from "@g1t/contracts";
The keeper reads Cloudflare as it really answers5
6import type { Route } from "./+types/usage";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit7import { Breakdown, IncludedAndCredit, UsageChart, UsageFilterBar, type UsageFilters, UsageSkeleton } from "../../components/usage";
8import { columns, defaultGrain, type Grain, type GroupBy, resolveRange } from "../../lib/usage";
Social cards for every page: og.g1t.sh9import { page } from "../../lib/meta";
The keeper reads Cloudflare as it really answers10import { billing } from "../../lib/services.server";
Merge main (membership, two-factor, GitHub repo roles) into tokens11import { getViewer, managesBilling, roleIn } from "../../lib/session.server";
The keeper reads Cloudflare as it really answers12
Usage, Billing settings and prepaid AI credit; fixes from the UX audit13/** Where the usage API is documented. */
14export const USAGE_API = "https://docs.g1t.sh/reference/api/#usage";
The keeper reads Cloudflare as it really answers15
Social cards for every page: og.g1t.sh16export function meta({ params, ...args }: Route.MetaArgs) {
17 return page(args, { title: `Usage · ${params.owner} · g1t` });
The keeper reads Cloudflare as it really answers18}
19
Usage, Billing settings and prepaid AI credit; fixes from the UX audit20/** The page's filters from its address: a filtered page is a link. */
21export function readFilters(url: URL, fixedProject?: string): UsageFilters {
22 const q = url.searchParams;
23 const { period, from, until } = resolveRange(q.get("period"), { from: q.get("from"), until: q.get("until") });
24 const known = new Set<string>(PRODUCTS.map((p) => p.key));
25 const grain = q.get("grain");
26 const group = q.get("group");
27 return {
28 period,
29 from,
30 until,
31 products: q.getAll("product").filter((p) => known.has(p)),
32 projects: fixedProject ? [fixedProject] : q.getAll("project").filter((p) => /^[a-z0-9._-]+\/[a-z0-9._-]+$/i.test(p)).map((p) => p.toLowerCase()),
33 group: group === "project" && !fixedProject ? "project" : group === "day" ? "day" : "product",
34 grain: grain === "day" || grain === "week" || grain === "month" ? grain : defaultGrain(from, until),
35 cumulative: q.get("cumulative") === "1",
36 };
The keeper reads Cloudflare as it really answers37}
38
Usage, Billing settings and prepaid AI credit; fixes from the UX audit39/** The address with one filter changed. */
40export function hrefWith(url: string, change: Record<string, string | null>): string {
41 const next = new URL(url);
42 for (const [key, value] of Object.entries(change)) {
43 if (value == null) next.searchParams.delete(key);
44 else next.searchParams.set(key, value);
45 }
46 return `${next.pathname}?${next.searchParams.toString()}`;
The keeper reads Cloudflare as it really answers47}
48
Usage, Billing settings and prepaid AI credit; fixes from the UX audit49export async function loader({ params, context, request }: Route.LoaderArgs) {
50 const viewer = getViewer(context);
51 const role = roleIn(viewer, params.owner);
52 if (!role) throw data(null, { status: 404 });
53 const slug = params.owner.toLowerCase();
54 const url = new URL(request.url);
55 const filters = readFilters(url);
56 // Streamed: the page's frame and filters come at once, each region's
57 // skeleton holds its place, and the report fills them in.
58 const report = billing
59 .usageReport(slug, viewer, { from: filters.from, until: filters.until, products: filters.products, projects: filters.projects })
60 .then((result) => (result.ok ? { report: result.value, error: null } : { report: null, error: result.error.message }));
Merge main (membership, two-factor, GitHub repo roles) into tokens61 return { slug, owner: managesBilling(getViewer(context), slug), filters, url: url.toString(), report };
The keeper reads Cloudflare as it really answers62}
63
Usage, Billing settings and prepaid AI credit; fixes from the UX audit64export function UsageView({
65 slug,
66 owner,
67 filters,
68 url,
69 report,
70 error,
71 fixedProject,
The keeper reads Cloudflare as it really answers72}: {
Usage, Billing settings and prepaid AI credit; fixes from the UX audit73 slug: string;
74 owner: boolean;
75 filters: UsageFilters;
76 url: string;
77 report: UsageReport | null;
78 error: string | null;
79 fixedProject?: string;
The keeper reads Cloudflare as it really answers80}) {
Usage, Billing settings and prepaid AI credit; fixes from the UX audit81 const bar = (
82 <UsageFilterBar
83 filters={filters}
84 products={[...PRODUCTS]}
85 projects={report?.projects ?? filters.projects}
86 report={report}
87 apiHref={USAGE_API}
88 fixedProject={fixedProject}
89 />
90 );
91 if (!report) {
92 return (
93 <div className="space-y-6">
94 {bar}
95 <p className="rounded-lg border border-danger/40 bg-danger/5 px-4 py-2.5 text-sm">{error ?? "Usage could not be read. Try again in a minute."}</p>
96 </div>
97 );
98 }
99 const cols = columns(report.days, report.from, report.until, filters.grain as Grain, filters.cumulative);
The keeper reads Cloudflare as it really answers100 return (
Usage, Billing settings and prepaid AI credit; fixes from the UX audit101 <div className="space-y-6">
102 {bar}
103 {!fixedProject && <IncludedAndCredit report={report} billingHref={`/${slug}/-/billing`} owner={owner} />}
104 <UsageChart
105 columns={cols}
106 products={report.products}
107 grain={filters.grain}
108 cumulative={filters.cumulative}
109 grainHref={(grain) => hrefWith(url, { grain })}
110 cumulativeHref={hrefWith(url, { cumulative: filters.cumulative ? null : "1" })}
111 />
112 <Breakdown report={report} group={filters.group as GroupBy} columns={cols} projectHref={fixedProject ? undefined : (project) => `/${project}/usage`} />
113 <p className="text-xs text-faint">
Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means.114 Every amount is usage at price: what was charged, plus what included usage, credit or a discount paid for it, to the cent; under a
115 cent reads &lt;$0.01. Storage, git operations, scans and search are metered through the month and counted in it as they go; they
116 are charged when it closes, on the same terms as the rest.
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens117 {!fixedProject && (
118 <>
119 {" "}
120 Each request your own code sent through the AI Gateway is listed on{" "}
121 <Link to={`/${slug}/-/gateway`} className="hover:text-fg hover:underline">
122 AI Gateway
123 </Link>
124 .
125 </>
126 )}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit127 </p>
128 </div>
The keeper reads Cloudflare as it really answers129 );
130}
131
Usage, Billing settings and prepaid AI credit; fixes from the UX audit132export default function UsagePage({ loaderData }: Route.ComponentProps) {
133 const { slug, owner, filters, url, report } = loaderData;
The keeper reads Cloudflare as it really answers134 return (
Usage, Billing settings and prepaid AI credit; fixes from the UX audit135 <Suspense
136 fallback={
137 <UsageSkeleton>
138 <UsageFilterBar filters={filters} products={[...PRODUCTS]} projects={filters.projects} report={null} apiHref={USAGE_API} />
139 </UsageSkeleton>
140 }
141 >
142 <Await resolve={report}>
143 {(loaded) => <UsageView slug={slug} owner={owner} filters={filters} url={url} report={loaded.report} error={loaded.error} />}
144 </Await>
145 </Suspense>
The keeper reads Cloudflare as it really answers146 );
147}

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