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 audit | 1 | import { Suspense } from "react"; |
| Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens | 2 | import { Await, Link, data } from "react-router"; |
| Share the editor settings and Claude Code deploy permissions | 3 | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 4 | import { PRODUCTS, type UsageReport } from "@g1t/contracts"; |
| Share the editor settings and Claude Code deploy permissions | 5 | |
| 6 | import type { Route } from "./+types/usage"; | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 7 | import { Breakdown, IncludedAndCredit, UsageChart, UsageFilterBar, type UsageFilters, UsageSkeleton } from "../../components/usage"; |
| 8 | import { columns, defaultGrain, type Grain, type GroupBy, resolveRange } from "../../lib/usage"; | |
| Social cards for every page: og.g1t.sh | 9 | import { page } from "../../lib/meta"; |
| Share the editor settings and Claude Code deploy permissions | 10 | import { billing } from "../../lib/services.server"; |
| Merge main (membership, two-factor, GitHub repo roles) into tokens | 11 | import { getViewer, managesBilling, roleIn } from "../../lib/session.server"; |
| Share the editor settings and Claude Code deploy permissions | 12 | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 13 | /** Where the usage API is documented. */ |
| 14 | export const USAGE_API = "https://docs.g1t.sh/reference/api/#usage"; | |
| Share the editor settings and Claude Code deploy permissions | 15 | |
| Social cards for every page: og.g1t.sh | 16 | export function meta({ params, ...args }: Route.MetaArgs) { |
| 17 | return page(args, { title: `Usage · ${params.owner} · g1t` }); | |
| Share the editor settings and Claude Code deploy permissions | 18 | } |
| 19 | ||
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 20 | /** The page's filters from its address: a filtered page is a link. */ |
| 21 | export 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 | }; | |
| Share the editor settings and Claude Code deploy permissions | 37 | } |
| 38 | ||
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 39 | /** The address with one filter changed. */ |
| 40 | export 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()}`; | |
| Share the editor settings and Claude Code deploy permissions | 47 | } |
| 48 | ||
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 49 | export 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 tokens | 61 | return { slug, owner: managesBilling(getViewer(context), slug), filters, url: url.toString(), report }; |
| Share the editor settings and Claude Code deploy permissions | 62 | } |
| 63 | ||
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 64 | export function UsageView({ |
| 65 | slug, | |
| 66 | owner, | |
| 67 | filters, | |
| 68 | url, | |
| 69 | report, | |
| 70 | error, | |
| 71 | fixedProject, | |
| Share the editor settings and Claude Code deploy permissions | 72 | }: { |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 73 | slug: string; |
| 74 | owner: boolean; | |
| 75 | filters: UsageFilters; | |
| 76 | url: string; | |
| 77 | report: UsageReport | null; | |
| 78 | error: string | null; | |
| 79 | fixedProject?: string; | |
| Share the editor settings and Claude Code deploy permissions | 80 | }) { |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 81 | 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); | |
| Share the editor settings and Claude Code deploy permissions | 100 | return ( |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 101 | <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 <$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 tokens | 117 | {!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 audit | 127 | </p> |
| 128 | </div> | |
| Share the editor settings and Claude Code deploy permissions | 129 | ); |
| 130 | } | |
| 131 | ||
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 132 | export default function UsagePage({ loaderData }: Route.ComponentProps) { |
| 133 | const { slug, owner, filters, url, report } = loaderData; | |
| Share the editor settings and Claude Code deploy permissions | 134 | return ( |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 135 | <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> | |
| Share the editor settings and Claude Code deploy permissions | 146 | ); |
| 147 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.