g1t/apps/sudo/app/routes/costs.tsx

453 lines22,267 bytesCodeBlame
1import type { ReactNode } from "react";
2import { Link } from "react-router";
3
4import type { CostsReport } from "@g1t/contracts";
5
6import type { Route } from "./+types/costs";
7import { DaysChart } from "~/components/costs";
8import { CostsHeader, chip, costsHref } from "~/components/costs-header";
9import { Badge, Button, Field, Input, Notice, Section, Stat, When } from "~/components/ui";
10import { capPercent, daySeries, marginTone, parseBucket, percentLabel, spendRows } from "~/lib/costs";
11import { type CostsActionResult, costsAction, costsLoader } from "~/lib/costs-route.server";
12import { usd } from "~/lib/money";
13
14export const meta: Route.MetaFunction = () => [{ title: "Costs & margin · sudo" }, { name: "robots", content: "noindex, nofollow" }];
15
16export const loader = ({ request, context }: Route.LoaderArgs) => costsLoader(request, context);
17export const action = ({ request, context }: Route.ActionArgs) => costsAction(request, context);
18
19/**
20 * Where g1t's money goes: the statement, what g1t pays for itself against
21 * its caps, and the same by day, by product and by workspace.
22 */
23export default function Costs({ loaderData, actionData }: Route.ComponentProps) {
24 const { range, report, error, done } = loaderData;
25 const failed = actionData && "error" in actionData ? (actionData as CostsActionResult) : null;
26 const description =
27 "What g1t cost to run (Cloudflare's bill and the model providers') against what workspaces paid, with what g1t gave away on purpose kept apart. Prices are cost plus 20%; the bill itself, drift and the price book are on Bill & pricing.";
28 if (!report) {
29 return (
30 <main className="mx-auto max-w-6xl px-4 py-8 sm:py-10">
31 <CostsHeader page="costs" range={range} report={null} done={null} runError={null} description={description} />
32 <div className="mt-5">
33 <Notice tone="warn">Billing did not answer for costs: {error}</Notice>
34 </div>
35 </main>
36 );
37 }
38 const bucket = parseBucket(loaderData.bucket, report.products.map((p) => p.bucket));
39 const product = report.products.find((p) => p.bucket === bucket) ?? null;
40 const series = daySeries(report.days, report.since, report.until, bucket);
41 const floor = report.settings.marginFloorPercent;
42 const open = report.proposals.filter((p) => p.status === "open");
43 return (
44 <main className="mx-auto max-w-6xl px-4 py-8 sm:py-10">
45 <CostsHeader page="costs" range={range} report={report} done={done} runError={failed?.section === "run" ? failed.error : null} description={description} />
46
47 <Statement report={report} floor={floor} range={range} proposals={open.length} />
48
49 <SpendSection caps={report.caps} error={failed?.section === "lift" ? failed.error : null} />
50
51 <Section
52 className="mt-6"
53 title={product ? `${product.title}, by day` : "By day"}
54 description={
55 product
56 ? "What customers were charged for it at price, before included usage and pools paid part, against what it cost."
57 : "Money in (usage paid for and the plan) against every cost. Pick a product to see its own."
58 }
59 >
60 <nav aria-label="Product" className="mb-4 flex flex-wrap gap-2">
61 <Link to={costsHref("costs", range, null)} aria-current={!bucket ? "page" : undefined} className={chip(!bucket)}>
62 All of g1t
63 </Link>
64 {report.products.map((p) => (
65 <Link key={p.bucket} to={costsHref("costs", range, p.bucket)} aria-current={p.bucket === bucket ? "page" : undefined} className={chip(p.bucket === bucket)}>
66 {p.title}
67 </Link>
68 ))}
69 </nav>
70 <DaysChart
71 days={series}
72 floor={floor}
73 revenueLabel={product ? "Charged at price" : "Money in"}
74 label={product ? `${product.title}: charged against Cloudflare cost by day` : "Money in against Cloudflare cost by day"}
75 />
76 </Section>
77
78 <Section className="mt-6" title="By product" description={`Each of g1t's products over the last ${range} days. The floor is ${floor}%.`}>
79 <div className="-mx-4 overflow-x-auto sm:-mx-5">
80 <table className="w-full min-w-[44rem] text-sm">
81 <thead>
82 <tr className="border-b border-line text-left text-xs text-muted">
83 <th className="px-4 py-2 font-medium sm:px-5">Product</th>
84 <th className="px-4 py-2 text-right font-medium">Charged at price</th>
85 <th className="px-4 py-2 text-right font-medium">Cost</th>
86 <th className="px-4 py-2 text-right font-medium">Price book said</th>
87 <th className="px-4 py-2 text-right font-medium">Margin</th>
88 <th className="px-4 py-2 text-right font-medium sm:pr-5">%</th>
89 </tr>
90 </thead>
91 <tbody>
92 {report.products.map((p) => {
93 const tone = p.overhead ? undefined : marginTone(p.marginPercent, floor);
94 return (
95 <tr key={p.bucket} className="border-b border-line last:border-0">
96 <td className="px-4 py-2.5 sm:px-5">
97 <Link to={costsHref("costs", range, p.bucket)} className="text-fg hover:underline">
98 {p.title}
99 </Link>
100 <span className="block text-xs text-faint">
101 {p.overhead ? "Paid by the plan" : p.costSource === "ledger" ? "Cost from the gateway, not Cloudflare" : p.bucket}
102 </span>
103 </td>
104 <td className="tabular px-4 py-2.5 text-right">{usd(p.valueMicros)}</td>
105 <td className="tabular px-4 py-2.5 text-right">{usd(p.costMicros)}</td>
106 <td className="tabular px-4 py-2.5 text-right text-muted">{usd(p.ownCostMicros)}</td>
107 <td className={`tabular px-4 py-2.5 text-right ${p.marginMicros < 0 && !p.overhead ? "text-danger" : ""}`}>{usd(p.marginMicros)}</td>
108 <td className="px-4 py-2.5 text-right sm:pr-5">
109 {p.overhead ? (
110 <span className="text-faint">—</span>
111 ) : (
112 <Badge tone={tone === "mint" ? "mint" : tone === "warn" ? "warn" : tone === "danger" ? "danger" : "plain"}>{percentLabel(p.marginPercent)}</Badge>
113 )}
114 </td>
115 </tr>
116 );
117 })}
118 </tbody>
119 </table>
120 </div>
121 </Section>
122
123 <Section
124 className="mt-6"
125 title="Workspaces that cost most"
126 description="Cloudflare's costs shared out by each workspace's own meters (its git operations, its usage), against what it paid. Over its revenue for 30 days, a workspace shows on Reach out."
127 >
128 {report.topWorkspaces.length === 0 ? (
129 <p className="text-sm text-muted">No costs shared out yet.</p>
130 ) : (
131 <div className="-mx-4 overflow-x-auto sm:-mx-5">
132 <table className="w-full min-w-[34rem] text-sm">
133 <thead>
134 <tr className="border-b border-line text-left text-xs text-muted">
135 <th className="px-4 py-2 font-medium sm:px-5">Workspace</th>
136 <th className="px-4 py-2 text-right font-medium">Cost to g1t</th>
137 <th className="px-4 py-2 text-right font-medium">Given away</th>
138 <th className="px-4 py-2 text-right font-medium">Paid</th>
139 <th className="px-4 py-2 text-right font-medium sm:pr-5">Net</th>
140 </tr>
141 </thead>
142 <tbody>
143 {report.topWorkspaces.map((w) => {
144 const net = w.revenueMicros - w.costMicros;
145 return (
146 <tr key={w.workspace} className="border-b border-line last:border-0">
147 <td className="px-4 py-2.5 sm:px-5">
148 <Link to={`/workspaces/${encodeURIComponent(w.workspace)}#billing`} className="font-mono text-fg hover:underline">
149 {w.workspace}
150 </Link>
151 {w.internal && <span className="ml-2 text-xs text-faint">g1t's own</span>}
152 </td>
153 <td className="tabular px-4 py-2.5 text-right">{usd(w.costMicros)}</td>
154 <td className="tabular px-4 py-2.5 text-right text-fg-soft">{usd(w.givenMicros ?? 0)}</td>
155 <td className="tabular px-4 py-2.5 text-right">{usd(w.revenueMicros)}</td>
156 <td className={`tabular px-4 py-2.5 text-right sm:pr-5 ${net < 0 && !w.internal ? "text-danger" : "text-fg-soft"}`}>{usd(net)}</td>
157 </tr>
158 );
159 })}
160 </tbody>
161 </table>
162 </div>
163 )}
164 </Section>
165
166 </main>
167 );
168}
169
170function CapMeter({ label, used, cap, hint }: { label: string; used: number; cap: number; hint: ReactNode }) {
171 const percent = capPercent(used, cap);
172 const tone = cap > 0 && used >= cap ? "bg-danger" : percent >= 75 ? "bg-warn" : "bg-merged";
173 return (
174 <div className="rounded-md border border-line px-4 py-3">
175 <div className="flex flex-wrap items-baseline justify-between gap-2">
176 <span className="text-sm font-medium">{label}</span>
177 <span className="tabular text-sm">
178 {usd(used)} <span className="text-faint">of {cap > 0 ? usd(cap) : "no cap"}</span>
179 </span>
180 </div>
181 <div className="mt-2 h-1.5 overflow-hidden rounded-full bg-line" role="meter" aria-valuemin={0} aria-valuemax={100} aria-valuenow={Math.round(percent)} aria-label={label}>
182 <div className={`h-full ${tone}`} style={{ width: `${percent}%` }} />
183 </div>
184 <p className="mt-1.5 text-xs text-muted">{hint}</p>
185 </div>
186 );
187}
188
189/**
190 * Where the money went, as a short statement: usage sold and running g1t
191 * (each against what paid for it), what g1t gave away on purpose (a budget,
192 * never a loss), and who was paid.
193 */
194function Statement({ report, floor, range, proposals }: { report: CostsReport; floor: number; range: number; proposals: number }) {
195 const o = report.overall;
196 // Reports from before the statement have only the totals.
197 const given = o.givenMicros ?? 0;
198 const usageCost = o.usageCostMicros ?? o.costMicros - given;
199 const included = o.includedMicros ?? 0;
200 const usageIn = o.usageMicros + included;
201 const usageMargin = o.usageMarginMicros ?? usageIn - usageCost;
202 const usagePercent = o.usageMarginPercent !== undefined ? o.usageMarginPercent : null;
203 const running = o.runningCostMicros ?? 0;
204 const unmapped = o.unmappedCostMicros ?? 0;
205 // Cloudflare's subscriptions are not on the usage bill: the estimate, over the range.
206 const subscriptions = Math.round((report.caps.fixedMonthlyMicros * range) / 30);
207 const moneyIn = o.usageMicros + o.plansMicros;
208 const spent = o.costMicros + subscriptions;
209 const net = moneyIn - spent;
210 const givenParts = [
211 ["comped", o.givenCompedMicros ?? 0],
212 ["free use", o.givenFreeMicros ?? 0],
213 ["trial", o.givenTrialMicros ?? 0],
214 ["open-source pool", o.givenPoolMicros ?? 0],
215 ].filter(([, micros]) => (micros as number) > 0) as [string, number][];
216 const rows: { title: string; note: string; in: number | null; cost: number; result: number | null; tone?: "danger" | "warn" | "muted" }[] = [
217 {
218 title: "Usage sold",
219 note:
220 included > 0
221 ? `What workspaces paid for usage (${usd(o.usageMicros, { cents: true })}) and their plan's included usage (${usd(included, { cents: true })}), against what it cost`
222 : "What workspaces paid for usage, against what that usage cost",
223 in: usageIn,
224 cost: usageCost,
225 result: usageMargin,
226 tone: usageMargin < 0 ? "danger" : undefined,
227 },
228 {
229 title: "Running g1t",
230 note:
231 included > 0
232 ? `Plans (${usd(o.plansMicros, { cents: true })}) less the included usage they paid for, against Workers, D1, KV, Queues and the rest`
233 : "Plans, against Workers, D1, KV, Queues and the rest of the platform",
234 in: o.plansMicros - included,
235 cost: running,
236 result: o.plansMicros - included - running,
237 tone: o.plansMicros - included - running < 0 ? "warn" : undefined,
238 },
239 {
240 title: "Cloudflare subscriptions",
241 note:
242 report.caps.fixedSource === "cloudflare"
243 ? `Fixed, as Cloudflare lists them${(report.caps.fixedItems ?? []).length > 0 ? ` (${(report.caps.fixedItems ?? []).map((i) => i.name).join(", ")})` : ""}; not on the usage bill`
244 : "Fixed, an estimate (CLOUDFLARE_FIXED_MONTHLY_MICROS) until Cloudflare's list is read; not on the usage bill",
245 in: null,
246 cost: subscriptions,
247 result: -subscriptions,
248 tone: "muted",
249 },
250 ...(unmapped > 0
251 ? [{ title: "Not mapped", note: "Billed by Cloudflare, charged for by nothing yet", in: null, cost: unmapped, result: -unmapped, tone: "danger" as const }]
252 : []),
253 {
254 title: "Given away",
255 note: givenParts.length > 0 ? givenParts.map(([why, micros]) => `${why} ${usd(micros, { cents: true })}`).join(" · ") : "Nothing this range",
256 in: null,
257 cost: given,
258 result: null,
259 tone: "muted",
260 },
261 ];
262 const resultClass = (tone?: string) => (tone === "danger" ? "text-danger" : tone === "warn" ? "text-warn" : tone === "muted" ? "text-muted" : "text-fg");
263 return (
264 <>
265 <div className="mt-5 grid grid-cols-2 gap-3 lg:grid-cols-4">
266 <Stat
267 label="Margin on usage sold"
268 value={percentLabel(usagePercent)}
269 hint={`${usd(usageIn, { cents: true })} paid for usage that cost ${usd(usageCost, { cents: true })}`}
270 tone={marginTone(usagePercent, floor)}
271 />
272 <Stat
273 label="Given away on purpose"
274 value={usd(given, { cents: true })}
275 hint={givenParts.length > 0 ? givenParts.map(([why, micros]) => `${usd(micros, { cents: true })} ${why}`).join(", ") : "Nothing given this range"}
276 />
277 <Stat
278 label="Who g1t paid"
279 value={usd(o.costMicros, { cents: true })}
280 hint={`Cloudflare ${usd(o.cloudflareCostMicros ?? o.costMicros - (o.modelsCostMicros ?? 0), { cents: true })}, model providers ${usd(o.modelsCostMicros ?? 0, { cents: true })}`}
281 />
282 <Stat
283 label="Proposals waiting"
284 value={String(proposals)}
285 hint={
286 <>
287 <Link to={costsHref("bill", range) + "#proposals"} className="underline underline-offset-2">
288 On Bill &amp; pricing
289 </Link>
290 {report.fetchedAt ? (
291 <>
292 {" "}
293 · bill read <When at={report.fetchedAt} time />
294 </>
295 ) : (
296 " · the bill has not been read yet"
297 )}
298 </>
299 }
300 tone={proposals > 0 ? "warn" : undefined}
301 />
302 </div>
303 <div className="mt-3 overflow-x-auto rounded-lg border border-line bg-surface">
304 <table className="w-full min-w-[36rem] text-sm">
305 <thead>
306 <tr className="border-b border-line text-left text-xs text-muted">
307 <th className="px-4 py-2 font-medium sm:px-5">
308 {report.since} to {report.until}
309 </th>
310 <th className="px-4 py-2 text-right font-medium">Paid in</th>
311 <th className="px-4 py-2 text-right font-medium">Cost</th>
312 <th className="px-4 py-2 text-right font-medium sm:pr-5">Result</th>
313 </tr>
314 </thead>
315 <tbody>
316 {rows.map((row) => (
317 <tr key={row.title} className="border-b border-line">
318 <td className="px-4 py-2.5 sm:px-5">
319 <span className="text-fg">{row.title}</span>
320 <span className="block text-xs text-faint">{row.note}</span>
321 </td>
322 <td className="tabular px-4 py-2.5 text-right">{row.in == null ? <span className="text-faint">—</span> : usd(row.in, { cents: true })}</td>
323 <td className="tabular px-4 py-2.5 text-right">{usd(row.cost, { cents: true })}</td>
324 <td className={`tabular px-4 py-2.5 text-right sm:pr-5 ${resultClass(row.tone)}`}>
325 {row.result == null ? "a budget" : usd(row.result, { signed: true, cents: true })}
326 </td>
327 </tr>
328 ))}
329 <tr>
330 <td className="px-4 py-2.5 sm:px-5">
331 <span className="font-medium text-fg">All in</span>
332 <span className="block text-xs text-faint">
333 {given > 0 ? `${usd(given, { cents: true })} of the cost was given away on purpose; without it, ${usd(net + given, { signed: true, cents: true })}` : "Everything above"}
334 </span>
335 </td>
336 <td className="tabular px-4 py-2.5 text-right font-medium">{usd(moneyIn, { cents: true })}</td>
337 <td className="tabular px-4 py-2.5 text-right font-medium">{usd(spent, { cents: true })}</td>
338 <td className="tabular px-4 py-2.5 text-right font-medium text-fg sm:pr-5">{usd(net, { signed: true, cents: true })}</td>
339 </tr>
340 </tbody>
341 </table>
342 </div>
343 </>
344 );
345}
346
347function SpendSection({ caps, error }: { caps: CostsReport["caps"]; error: string | null }) {
348 const { rows, totalMicros } = spendRows(caps);
349 const net = caps.revenueMicros - totalMicros;
350 return (
351 <Section
352 className="mt-6"
353 id="spend"
354 title="g1t's own spend"
355 description="What g1t pays for itself, at cost: comped accounts, the trial and open-source pools, free workspaces' overruns, and anything charged without real money behind it. Two caps hold it: each comped account's monthly budget, and a daily breaker on all of it that pauses new hosted-model agent runs g1t would pay for."
356 >
357 <div className="grid gap-3 lg:grid-cols-2">
358 <CapMeter
359 label={`Today, ${caps.day} (UTC)`}
360 used={caps.todayMicros}
361 cap={caps.dailyCapMicros}
362 hint={
363 caps.tripped ? (
364 <span className="text-danger">
365 Breaker open{caps.trippedAt ? <> since <When at={caps.trippedAt} time /></> : null}: new hosted-model runs g1t pays for wait until 00:00 UTC.
366 </span>
367 ) : caps.liftedBy ? (
368 <>
369 Lifted for today by {caps.liftedBy}
370 {caps.liftNote ? `: “${caps.liftNote}”` : ""}.
371 </>
372 ) : (
373 "Paying workspaces on a live card are never paused. PLATFORM_DAILY_SPEND_CAP_MICROS."
374 )
375 }
376 />
377 {caps.comped.map((b) => (
378 <CapMeter
379 key={b.account}
380 label={`${b.name}, ${caps.month} (comped)`}
381 used={b.usedMicros}
382 cap={b.ceilingMicros}
383 hint={
384 <>
385 {b.ceilingMicros > 0 && b.usedMicros >= b.ceilingMicros ? (
386 <span className="text-danger">Used up: new work on it is refused. </span>
387 ) : null}
388 {b.defaultCeiling ? "The default budget (COMPED_MONTHLY_CEILING_MICROS)" : "Its own budget, in its terms"}. Raise it on{" "}
389 <Link to={`/workspaces/${encodeURIComponent(b.name)}#billing`} className="underline underline-offset-2">
390 its account
391 </Link>
392 : Terms, Limit.
393 </>
394 }
395 />
396 ))}
397 </div>
398 {(caps.tripped || error) && (
399 <form method="post" action="#spend" className="mt-4 flex flex-col gap-2 sm:flex-row sm:items-end">
400 <input type="hidden" name="intent" value="lift" />
401 <Field label="Why lift it" hint="Recorded in the audit log.">
402 <Input name="note" required minLength={5} maxLength={500} placeholder="e.g. Launch day; watching it" className="sm:min-w-[24rem]" />
403 </Field>
404 <Button type="submit" variant="danger">
405 Lift for today
406 </Button>
407 </form>
408 )}
409 {error && (
410 <div className="mt-3">
411 <Notice tone="error">{error}</Notice>
412 </div>
413 )}
414 <div className="-mx-4 mt-5 overflow-x-auto sm:-mx-5">
415 <table className="w-full min-w-[34rem] text-sm">
416 <thead>
417 <tr className="border-b border-line text-left text-xs text-muted">
418 <th className="px-4 py-2 font-medium sm:px-5">{caps.month}, so far</th>
419 <th className="px-4 py-2 text-right font-medium sm:pr-5">g1t paid</th>
420 </tr>
421 </thead>
422 <tbody>
423 {rows.map((row) => (
424 <tr key={row.key} className="border-b border-line align-top">
425 <td className="px-4 py-2.5 sm:px-5">
426 {row.title}
427 <span className="block text-xs text-faint">{row.note}</span>
428 </td>
429 <td className="tabular px-4 py-2.5 text-right sm:pr-5">{usd(row.micros)}</td>
430 </tr>
431 ))}
432 <tr className="border-b border-line font-medium">
433 <td className="px-4 py-2.5 sm:px-5">All of it</td>
434 <td className="tabular px-4 py-2.5 text-right sm:pr-5">{usd(totalMicros)}</td>
435 </tr>
436 <tr className="border-b border-line">
437 <td className="px-4 py-2.5 sm:px-5">
438 Money in
439 <span className="block text-xs text-faint">Usage paid for and the plan, reconciled through yesterday</span>
440 </td>
441 <td className="tabular px-4 py-2.5 text-right sm:pr-5">{usd(caps.revenueMicros)}</td>
442 </tr>
443 <tr>
444 <td className="px-4 py-2.5 sm:px-5">Money in less what g1t paid</td>
445 <td className={`tabular px-4 py-2.5 text-right sm:pr-5 ${net < 0 ? "text-danger" : "text-fg-soft"}`}>{usd(net, { signed: true })}</td>
446 </tr>
447 </tbody>
448 </table>
449 </div>
450 </Section>
451 );
452}
453