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.
| The statement is a month at a time, a line per kind of charge | 1 | import { ChevronRight, Download } from "lucide-react"; |
| 2 | import { useState } from "react"; | |
| 3 | import { Link, useFetcher, useNavigate } from "react-router"; | |
| 4 | ||
| 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. | 5 | import type { LedgerEntry, Statement } from "@g1t/contracts"; |
| 6 | import { money } from "../lib/money"; | |
| The statement is a month at a time, a line per kind of charge | 7 | |
| 8 | import { EmptyState, TimeAgo } from "./ui"; | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 9 | import { Button } from "./ui/button"; |
| 10 | import { Card } from "./ui/card"; | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 11 | import { SkeletonRows } from "./ui/skeleton"; |
| The statement is a month at a time, a line per kind of charge | 12 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; |
| 13 | import { Tabs, TabsList, TabsTrigger } from "./ui/tabs"; | |
| 14 | ||
| 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. | 15 | /** A charge, with the fraction of a cent it carries (`lib/money.ts`): a statement adds up to the micro. */ |
| The statement is a month at a time, a line per kind of charge | 16 | function charge(micros: number): string { |
| 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. | 17 | return money(micros, { precise: true }); |
| The statement is a month at a time, a line per kind of charge | 18 | } |
| 19 | ||
| Billing: credits with a kind and expiry, discounts instead of comped, and safer charging | 20 | /** A usage entry at its price: what was charged, what paid for it first, and what the discount took off. */ |
| 21 | export function entryPrice(entry: Pick<LedgerEntry, "amountMicros" | "creditMicros" | "trialMicros" | "ossMicros" | "givenMicros" | "discountMicros">): number { | |
| 22 | if ((entry.discountMicros ?? 0) === 0) return -entry.amountMicros; | |
| 23 | return -entry.amountMicros + (entry.creditMicros ?? 0) + (entry.trialMicros ?? 0) + (entry.ossMicros ?? 0) + (entry.givenMicros ?? 0) + (entry.discountMicros ?? 0); | |
| 24 | } | |
| 25 | ||
| 26 | /** `Discount (100%)`, or `Discount` when the percentage is not known (a discount since ended). */ | |
| 27 | export function discountName(percent: number | null | undefined): string { | |
| 28 | return percent ? `Discount (${percent}%)` : "Discount"; | |
| 29 | } | |
| 30 | ||
| The statement is a month at a time, a line per kind of charge | 31 | function monthLabel(month: string): string { |
| 32 | const [year, number] = month.split("-").map(Number); | |
| 33 | return new Date(Date.UTC(year, number - 1, 1)).toLocaleDateString("en-US", { | |
| 34 | month: "long", | |
| 35 | year: "numeric", | |
| 36 | timeZone: "UTC", | |
| 37 | }); | |
| 38 | } | |
| 39 | ||
| 40 | function dayLabel(day: string): string { | |
| 41 | return new Date(`${day}T00:00:00Z`).toLocaleDateString("en-US", { | |
| 42 | weekday: "short", | |
| 43 | month: "short", | |
| 44 | day: "numeric", | |
| 45 | timeZone: "UTC", | |
| 46 | }); | |
| 47 | } | |
| 48 | ||
| 49 | /** | |
| 50 | * A month of the workspace's ledger: totals, then a group per day (or per | |
| 51 | * project) with one line per kind of charge. A line opens to its entries, | |
| 52 | * 50 at a time. | |
| 53 | */ | |
| 54 | export function StatementView({ | |
| 55 | slug, | |
| 56 | statement, | |
| 57 | group, | |
| 58 | }: { | |
| 59 | slug: string; | |
| 60 | statement: Statement; | |
| 61 | group: "day" | "project"; | |
| 62 | }) { | |
| 63 | const navigate = useNavigate(); | |
| 64 | const go = (month: string, by: string) => | |
| 65 | navigate(`/${slug}/-/billing?month=${month}&group=${by}#statement`, { preventScrollReset: true }); | |
| 66 | const months = statement.months.includes(statement.month) ? statement.months : [statement.month, ...statement.months]; | |
| 67 | const { totals } = statement; | |
| Billing: credits with a kind and expiry, discounts instead of comped, and safer charging | 68 | // With a discount, every line at its price, and the discount beside it. |
| 69 | const atPrice = (totals.discountMicros ?? 0) > 0; | |
| 70 | const discountLabel = discountName(totals.discountPercent); | |
| The statement is a month at a time, a line per kind of charge | 71 | |
| 72 | return ( | |
| 73 | <section id="statement" className="mt-10 scroll-mt-20"> | |
| 74 | <div className="flex flex-wrap items-center gap-3"> | |
| 75 | <h3 className="grow text-sm font-medium text-muted">Statement</h3> | |
| 76 | <Select value={statement.month} onValueChange={(month) => go(month, group)}> | |
| 77 | <SelectTrigger size="sm" className="w-40" aria-label="Month"> | |
| 78 | <SelectValue /> | |
| 79 | </SelectTrigger> | |
| 80 | <SelectContent> | |
| 81 | {months.map((month) => ( | |
| 82 | <SelectItem key={month} value={month}> | |
| 83 | {monthLabel(month)} | |
| 84 | </SelectItem> | |
| 85 | ))} | |
| 86 | </SelectContent> | |
| 87 | </Select> | |
| 88 | <Tabs value={group} onValueChange={(by) => go(statement.month, by)}> | |
| 89 | <TabsList> | |
| 90 | <TabsTrigger value="day">By day</TabsTrigger> | |
| 91 | <TabsTrigger value="project">By project</TabsTrigger> | |
| 92 | </TabsList> | |
| 93 | </Tabs> | |
| 94 | {totals.entries > 0 && ( | |
| 95 | <a | |
| 96 | href={`/${slug}/-/billing/statement.csv?month=${statement.month}`} | |
| 97 | className="flex h-8 items-center gap-1.5 rounded-md border border-line px-2.5 text-[0.8125rem] text-muted hover:border-line-strong hover:text-fg" | |
| 98 | download | |
| 99 | > | |
| 100 | <Download size={13} /> CSV | |
| 101 | </a> | |
| 102 | )} | |
| 103 | </div> | |
| 104 | ||
| Billing: credits with a kind and expiry, discounts instead of comped, and safer charging | 105 | {atPrice ? ( |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 106 | <Card asChild className="mt-3 grid grid-cols-2 divide-line text-sm sm:grid-cols-4 sm:divide-x"> |
| 107 | <dl> | |
| 108 | <div className="px-4 py-3"> | |
| 109 | <dt className="text-xs text-faint">Usage at price</dt> | |
| 110 | <dd className="mt-0.5 font-mono tabular-nums">{charge(totals.priceMicros ?? 0)}</dd> | |
| 111 | </div> | |
| 112 | <div className="px-4 py-3"> | |
| 113 | <dt className="text-xs text-faint">{discountLabel}</dt> | |
| 114 | <dd className="mt-0.5 font-mono tabular-nums text-success">{charge(-(totals.discountMicros ?? 0))}</dd> | |
| 115 | </div> | |
| 116 | <div className="px-4 py-3"> | |
| 117 | <dt className="text-xs text-faint">Charged</dt> | |
| 118 | <dd className="mt-0.5 font-mono tabular-nums">{charge(totals.chargedMicros)}</dd> | |
| 119 | </div> | |
| 120 | <div className="px-4 py-3"> | |
| 121 | <dt className="text-xs text-faint">Paid and credited</dt> | |
| 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. | 122 | <dd className="mt-0.5 font-mono tabular-nums">{money(totals.paidMicros)}</dd> |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 123 | </div> |
| 124 | </dl> | |
| 125 | </Card> | |
| Billing: credits with a kind and expiry, discounts instead of comped, and safer charging | 126 | ) : ( |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 127 | <Card asChild divided className="mt-3 grid grid-cols-1 sm:grid-cols-3 sm:divide-x sm:divide-y-0 text-sm"> |
| 128 | <dl> | |
| 129 | <div className="px-4 py-3"> | |
| 130 | <dt className="text-xs text-faint">Charged</dt> | |
| 131 | <dd className="mt-0.5 font-mono tabular-nums">{charge(totals.chargedMicros)}</dd> | |
| 132 | </div> | |
| 133 | <div className="px-4 py-3"> | |
| 134 | <dt className="text-xs text-faint">Paid and credited</dt> | |
| 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. | 135 | <dd className="mt-0.5 font-mono tabular-nums">{money(totals.paidMicros)}</dd> |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 136 | </div> |
| 137 | <div className="px-4 py-3"> | |
| 138 | <dt className="text-xs text-faint">Entries</dt> | |
| 139 | <dd className="mt-0.5 font-mono tabular-nums">{totals.entries.toLocaleString("en-US")}</dd> | |
| 140 | </div> | |
| 141 | </dl> | |
| 142 | </Card> | |
| Billing: credits with a kind and expiry, discounts instead of comped, and safer charging | 143 | )} |
| The statement is a month at a time, a line per kind of charge | 144 | |
| Merge Stripe Tax, the card fee on card payments, and one free workspace per person | 145 | {((totals.taxMicros ?? 0) !== 0 || (totals.cardFeeMicros ?? 0) !== 0) && ( |
| 146 | <p className="mt-2 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. | 147 | Paid with this month's payments on top of what they credited: tax {money(totals.taxMicros ?? 0)}, card processing fees{" "} |
| 148 | {money(totals.cardFeeMicros ?? 0)}. Prices exclude tax; neither comes from your balance. | |
| Merge Stripe Tax, the card fee on card payments, and one free workspace per person | 149 | </p> |
| 150 | )} | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 151 | {((totals.covered?.length ?? 0) > 0 || (totals.carriedMicros ?? 0) > 0) && ( |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 152 | <Card asChild className="mt-2 space-y-1 px-4 py-3 text-sm"> |
| 153 | <ul> | |
| 154 | {totals.covered?.map((paid) => ( | |
| 155 | <li key={paid.source} className="flex justify-between gap-4"> | |
| 156 | <span className="text-muted">{paid.label}</span> | |
| 157 | <span className="font-mono tabular-nums text-success">{charge(paid.micros)}</span> | |
| 158 | </li> | |
| 159 | ))} | |
| 160 | {(totals.carriedMicros ?? 0) > 0 && ( | |
| 161 | <li className="flex justify-between gap-4"> | |
| 162 | <span className="text-muted">Under the minimum charge, so carried over to the next invoice</span> | |
| 163 | <span className="font-mono tabular-nums">{charge(totals.carriedMicros ?? 0)}</span> | |
| 164 | </li> | |
| 165 | )} | |
| 166 | </ul> | |
| 167 | </Card> | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 168 | )} |
| 169 | ||
| The statement is a month at a time, a line per kind of charge | 170 | <div className="mt-3"> |
| 171 | {statement.groups.length === 0 ? ( | |
| 172 | <EmptyState title={`Nothing in ${monthLabel(statement.month)}`}> | |
| 173 | Agent runs, sandbox time, deployments and payments appear here, a line per kind each day. | |
| 174 | </EmptyState> | |
| 175 | ) : ( | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 176 | <Card tone="plain" className="overflow-hidden"> |
| The statement is a month at a time, a line per kind of charge | 177 | {statement.groups.map((g) => ( |
| 178 | <div key={g.key} className="border-b border-line last:border-b-0"> | |
| 179 | <div className="flex items-center gap-4 bg-surface px-4 py-2 text-xs"> | |
| 180 | <span className="grow truncate font-medium text-muted"> | |
| 181 | {group === "day" ? dayLabel(g.key) : g.label} | |
| 182 | </span> | |
| 183 | <span className="font-mono tabular-nums text-faint">{charge(g.chargedMicros)}</span> | |
| 184 | </div> | |
| 185 | <ul className="divide-y divide-line"> | |
| Merge Stripe Tax, the card fee on card payments, and one free workspace per person | 186 | {g.lines.map((line) => |
| 187 | line.kind === "Tax" || line.kind === "Card processing fees" ? ( | |
| 188 | // Paid with the day's payments, on top of what reached the balance: never charged. | |
| 189 | <li key={line.kind} className="flex items-center gap-3 px-4 py-3 text-sm"> | |
| 190 | <span className="w-3.5 shrink-0" /> | |
| 191 | <span className="grow truncate text-muted"> | |
| 192 | {line.kind} <span className="text-xs text-faint">· paid with the payment, not from the balance</span> | |
| 193 | </span> | |
| 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. | 194 | <span className="w-24 shrink-0 text-right font-mono tabular-nums text-muted">{money(line.passedMicros ?? 0)}</span> |
| Merge Stripe Tax, the card fee on card payments, and one free workspace per person | 195 | </li> |
| 196 | ) : ( | |
| The statement is a month at a time, a line per kind of charge | 197 | <StatementLineRow |
| 198 | key={line.kind} | |
| 199 | slug={slug} | |
| 200 | month={statement.month} | |
| 201 | kind={line.kind} | |
| 202 | count={line.count} | |
| Billing: credits with a kind and expiry, discounts instead of comped, and safer charging | 203 | chargedMicros={atPrice && line.chargedMicros >= 0 ? (line.priceMicros ?? line.chargedMicros) : line.chargedMicros} |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 204 | coveredMicros={line.coveredMicros ?? 0} |
| The statement is a month at a time, a line per kind of charge | 205 | day={group === "day" ? g.key : null} |
| 206 | project={group === "project" ? g.key : null} | |
| 207 | /> | |
| Merge Stripe Tax, the card fee on card payments, and one free workspace per person | 208 | ), |
| 209 | )} | |
| Billing: credits with a kind and expiry, discounts instead of comped, and safer charging | 210 | {(g.discountMicros ?? 0) > 0 && ( |
| 211 | <li className="flex items-center gap-3 px-4 py-3 text-sm"> | |
| 212 | <span className="w-3.5 shrink-0" /> | |
| 213 | <span className="grow truncate text-muted">{discountLabel}</span> | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 214 | <span className="w-24 shrink-0 text-right font-mono tabular-nums text-success">{charge(-(g.discountMicros ?? 0))}</span> |
| Billing: credits with a kind and expiry, discounts instead of comped, and safer charging | 215 | </li> |
| 216 | )} | |
| The statement is a month at a time, a line per kind of charge | 217 | </ul> |
| 218 | </div> | |
| 219 | ))} | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 220 | </Card> |
| The statement is a month at a time, a line per kind of charge | 221 | )} |
| 222 | </div> | |
| 223 | </section> | |
| 224 | ); | |
| 225 | } | |
| 226 | ||
| 227 | function StatementLineRow({ | |
| 228 | slug, | |
| 229 | month, | |
| 230 | kind, | |
| 231 | count, | |
| 232 | chargedMicros, | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 233 | coveredMicros, |
| The statement is a month at a time, a line per kind of charge | 234 | day, |
| 235 | project, | |
| 236 | }: { | |
| 237 | slug: string; | |
| 238 | month: string; | |
| 239 | kind: string; | |
| 240 | count: number; | |
| 241 | chargedMicros: number; | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 242 | coveredMicros: number; |
| The statement is a month at a time, a line per kind of charge | 243 | day: string | null; |
| 244 | project: string | null; | |
| 245 | }) { | |
| 246 | const [open, setOpen] = useState(false); | |
| 247 | const [entries, setEntries] = useState<LedgerEntry[]>([]); | |
| 248 | const [done, setDone] = useState(false); | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 249 | const fetcher = useFetcher<LedgerEntry[] | { error: string }>(); |
| The statement is a month at a time, a line per kind of charge | 250 | const [seen, setSeen] = useState<LedgerEntry[] | undefined>(undefined); |
| Merge branch 'worktree-agent-a985c01f43538af01' | 251 | const failed = fetcher.data && !Array.isArray(fetcher.data) ? fetcher.data.error : null; |
| The statement is a month at a time, a line per kind of charge | 252 | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 253 | // Each page that arrives is added once. A page can come again: the page |
| 254 | // reloads what fetchers loaded after every change made on it, so an entry | |
| 255 | // already listed is not listed twice. | |
| 256 | if (Array.isArray(fetcher.data) && fetcher.data !== seen) { | |
| 257 | const page = fetcher.data; | |
| 258 | setSeen(page); | |
| 259 | setEntries((before) => { | |
| 260 | const listed = new Set(before.map((entry) => entry.id)); | |
| 261 | return [...before, ...page.filter((entry) => !listed.has(entry.id))]; | |
| 262 | }); | |
| 263 | if (page.length < 50) setDone(true); | |
| The statement is a month at a time, a line per kind of charge | 264 | } |
| 265 | ||
| 266 | const load = (before: string | null) => { | |
| 267 | const query = new URLSearchParams({ month, kind }); | |
| 268 | if (day) query.set("day", day); | |
| 269 | if (project !== null) query.set("project", project); | |
| 270 | if (before) query.set("before", before); | |
| 271 | fetcher.load(`/${slug}/-/billing/entries?${query}`); | |
| 272 | }; | |
| 273 | ||
| 274 | const toggle = () => { | |
| 275 | if (!open && entries.length === 0) load(null); | |
| 276 | setOpen(!open); | |
| 277 | }; | |
| 278 | const moneyIn = chargedMicros < 0; | |
| 279 | ||
| 280 | return ( | |
| 281 | <li> | |
| 282 | <button | |
| 283 | type="button" | |
| 284 | onClick={toggle} | |
| 285 | aria-expanded={open} | |
| 286 | className="flex w-full items-center gap-3 px-4 py-3 text-left text-sm hover:bg-surface/60" | |
| 287 | > | |
| 288 | <ChevronRight size={14} className={`shrink-0 text-faint transition-transform ${open ? "rotate-90" : ""}`} /> | |
| 289 | <span className="grow truncate font-medium">{kind}</span> | |
| 290 | <span className="shrink-0 text-xs text-faint tabular-nums"> | |
| 291 | {count.toLocaleString("en-US")} {count === 1 ? "entry" : "entries"} | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 292 | {coveredMicros > 0 && ` · ${charge(coveredMicros)} paid for`} |
| The statement is a month at a time, a line per kind of charge | 293 | </span> |
| 294 | <span | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 295 | className={`w-24 shrink-0 text-right font-mono tabular-nums ${moneyIn ? "text-success" : "text-fg"}`} |
| The statement is a month at a time, a line per kind of charge | 296 | > |
| 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. | 297 | {moneyIn ? `+${money(-chargedMicros)}` : charge(chargedMicros)} |
| The statement is a month at a time, a line per kind of charge | 298 | </span> |
| 299 | </button> | |
| 300 | {open && ( | |
| 301 | <div className="border-t border-line bg-bg/40 pb-2"> | |
| 302 | <ul className="divide-y divide-line/60"> | |
| 303 | {entries.map((entry) => ( | |
| 304 | <li key={entry.id} className="flex items-center gap-4 py-2 pr-4 pl-11 text-[0.8125rem]"> | |
| 305 | <div className="min-w-0 grow"> | |
| 306 | {entry.repo && entry.number ? ( | |
| 307 | <Link to={`/${entry.repo}/pull/${entry.number}`} className="block truncate hover:underline"> | |
| 308 | {entry.description} | |
| 309 | </Link> | |
| 310 | ) : ( | |
| 311 | <p className="truncate">{entry.description}</p> | |
| 312 | )} | |
| 313 | <p className="mt-0.5 text-xs text-faint"> | |
| 314 | <TimeAgo at={entry.createdAt} /> | |
| 315 | {entry.model && ` · ${entry.model}`} | |
| 316 | {entry.createdBy && ` · ${entry.createdBy}`} | |
| 317 | </p> | |
| 318 | </div> | |
| Billing: credits with a kind and expiry, discounts instead of comped, and safer charging | 319 | <span className="shrink-0 text-right font-mono text-xs tabular-nums text-muted"> |
| 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. | 320 | {entry.amountMicros > 0 ? `+${money(entry.amountMicros)}` : charge(entryPrice(entry))} |
| Billing: credits with a kind and expiry, discounts instead of comped, and safer charging | 321 | {(entry.discountMicros ?? 0) > 0 && ( |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 322 | <span className="block text-[0.6875rem] text-success">{charge(-(entry.discountMicros ?? 0))} discount</span> |
| Billing: credits with a kind and expiry, discounts instead of comped, and safer charging | 323 | )} |
| The statement is a month at a time, a line per kind of charge | 324 | </span> |
| 325 | </li> | |
| 326 | ))} | |
| 327 | </ul> | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 328 | {failed && fetcher.state === "idle" && <p className="py-2 pl-11 text-xs text-danger">{failed}</p>} |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 329 | {fetcher.state === "loading" && ( |
| 330 | <div aria-busy="true" className="pl-8"> | |
| 331 | <SkeletonRows rows={3} rowClassName="h-12" /> | |
| 332 | </div> | |
| 333 | )} | |
| The statement is a month at a time, a line per kind of charge | 334 | {!done && entries.length > 0 && entries.length < count && fetcher.state === "idle" && ( |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 335 | <Button |
| The statement is a month at a time, a line per kind of charge | 336 | type="button" |
| 337 | onClick={() => load(entries[entries.length - 1].id)} | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 338 | variant="link" |
| 339 | size="inline" | |
| 340 | className="mt-1 ml-11 text-xs text-muted hover:text-fg font-normal" | |
| The statement is a month at a time, a line per kind of charge | 341 | > |
| 342 | Show {Math.min(50, count - entries.length)} more of {count - entries.length} | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 343 | </Button> |
| The statement is a month at a time, a line per kind of charge | 344 | )} |
| 345 | </div> | |
| 346 | )} | |
| 347 | </li> | |
| 348 | ); | |
| 349 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.