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.

737 linesCodeBlameRaw
1import type { ReactNode } from "react";
2import { Link } from "react-router";
3
4import type { CostsReport, PlatformGuard, PlatformMetric } 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 {
11 PAUSE_LEVELS,
12 capPercent,
13 count,
14 daySeries,
15 givenParts as givenBy,
16 marginOnPrice,
17 marginTone,
18 parseBucket,
19 percentLabel,
20 spendRows,
21 subscriptionsOver,
22 thresholdShare,
23 whoPaid,
24} from "~/lib/costs";
25import { type CostsActionResult, costsAction, costsLoader } from "~/lib/costs-route.server";
26import { usd } from "~/lib/money";
27
28export const meta: Route.MetaFunction = () => [{ title: "Costs & margin · sudo" }, { name: "robots", content: "noindex, nofollow" }];
29
30/** Billing's MARGIN_PERCENT: what is added to cost. */
31const MARKUP_PERCENT = 20;
32
33export const loader = ({ request, context }: Route.LoaderArgs) => costsLoader(request, context);
34export const action = ({ request, context }: Route.ActionArgs) => costsAction(request, context);
35
36/**
37 * Where g1t's money goes: the statement, what g1t pays for itself against
38 * its caps, and the same by day, by product and by workspace.
39 */
40export default function Costs({ loaderData, actionData }: Route.ComponentProps) {
41 const { range, report, error, done } = loaderData;
42 const failed = actionData && "error" in actionData ? (actionData as CostsActionResult) : null;
43 const description = `What g1t cost to run (Cloudflare's bill) against what workspaces paid, with what g1t gave away on purpose kept apart. Prices are cost plus ${MARKUP_PERCENT}%, a ${percentLabel(marginOnPrice(MARKUP_PERCENT))} margin: every margin here is a share of the price, not of the cost. The bill itself, drift and the price book are on Bill & pricing.`;
44 if (!report) {
45 return (
46 <main className="mx-auto max-w-6xl px-4 py-8 sm:py-10">
47 <CostsHeader page="costs" range={range} report={null} done={null} runError={null} description={description} />
48 <div className="mt-5">
49 <Notice tone="warn">Billing did not answer for costs: {error}</Notice>
50 </div>
51 <PlatformSection guard={loaderData.guard} unavailable={loaderData.guardError} failed={failed} />
52 </main>
53 );
54 }
55 const bucket = parseBucket(
56 loaderData.bucket,
57 report.products.map((p) => p.bucket),
58 );
59 const product = report.products.find((p) => p.bucket === bucket) ?? null;
60 const series = daySeries(report.days, report.since, report.until, bucket);
61 const floor = report.settings.marginFloorPercent;
62 const open = report.proposals.filter((p) => p.status === "open");
63 return (
64 <main className="mx-auto max-w-6xl px-4 py-8 sm:py-10">
65 <CostsHeader
66 page="costs"
67 range={range}
68 report={report}
69 done={done}
70 runError={failed?.section === "run" ? failed.error : null}
71 description={description}
72 />
73
74 <Statement report={report} floor={floor} range={range} proposals={open.length} />
75
76 <SpendSection caps={report.caps} range={range} error={failed?.section === "lift" ? failed.error : null} />
77
78 <PlatformSection guard={loaderData.guard} unavailable={loaderData.guardError} failed={failed} />
79
80 <Section
81 className="mt-6"
82 title={product ? `${product.title}, by day` : "By day"}
83 description={
84 product
85 ? "What customers were charged for it at price, before included usage and pools paid part, against what it cost."
86 : "Money in (usage paid for and the plan) against every cost. Pick a product to see its own."
87 }
88 >
89 <nav aria-label="Product" className="mb-4 flex flex-wrap gap-2">
90 <Link to={costsHref("costs", range, null)} aria-current={!bucket ? "page" : undefined} className={chip(!bucket)}>
91 All of g1t
92 </Link>
93 {report.products.map((p) => (
94 <Link
95 key={p.bucket}
96 to={costsHref("costs", range, p.bucket)}
97 aria-current={p.bucket === bucket ? "page" : undefined}
98 className={chip(p.bucket === bucket)}
99 >
100 {p.title}
101 </Link>
102 ))}
103 </nav>
104 <DaysChart
105 days={series}
106 floor={floor}
107 revenueLabel={product ? "Charged at price" : "Money in"}
108 label={product ? `${product.title}: charged against Cloudflare cost by day` : "Money in against Cloudflare cost by day"}
109 />
110 </Section>
111
112 <Section
113 className="mt-6"
114 title="By product"
115 description={`Each of g1t's products over the last ${range} days: what customers were charged at price, given away or not, against what it cost. The margin is the charge less the cost, as a share of the charge; at cost plus ${MARKUP_PERCENT}% it is ${percentLabel(marginOnPrice(MARKUP_PERCENT))}. The floor is ${floor}%.`}
116 >
117 <div className="-mx-4 overflow-x-auto sm:-mx-5">
118 <table className="w-full min-w-176 text-sm">
119 <thead>
120 <tr className="border-b border-line text-left text-xs text-muted">
121 <th className="px-4 py-2 font-medium sm:px-5">Product</th>
122 <th className="px-4 py-2 text-right font-medium">Charged at price</th>
123 <th className="px-4 py-2 text-right font-medium">Cost</th>
124 <th className="px-4 py-2 text-right font-medium">Price book said</th>
125 <th className="px-4 py-2 text-right font-medium">Margin</th>
126 <th className="px-4 py-2 text-right font-medium sm:pr-5">% of price</th>
127 </tr>
128 </thead>
129 <tbody>
130 {report.products.map((p) => {
131 const tone = p.overhead ? undefined : marginTone(p.marginPercent, floor);
132 return (
133 <tr key={p.bucket} className="border-b border-line last:border-0">
134 <td className="px-4 py-2.5 sm:px-5">
135 <Link to={costsHref("costs", range, p.bucket)} className="text-fg hover:underline">
136 {p.title}
137 </Link>
138 <span className="block text-xs text-faint">
139 {p.overhead ? "Paid by the plan" : p.costSource === "ledger" ? "Cost from the gateway, not Cloudflare" : p.bucket}
140 </span>
141 </td>
142 <td className="tabular px-4 py-2.5 text-right">{usd(p.valueMicros)}</td>
143 <td className="tabular px-4 py-2.5 text-right">{usd(p.costMicros)}</td>
144 <td className="tabular px-4 py-2.5 text-right text-muted">{usd(p.ownCostMicros)}</td>
145 <td className={`tabular px-4 py-2.5 text-right ${p.marginMicros < 0 && !p.overhead ? "text-danger" : ""}`}>{usd(p.marginMicros)}</td>
146 <td className="px-4 py-2.5 text-right sm:pr-5">
147 {p.overhead ? (
148 <span className="text-faint">—</span>
149 ) : (
150 <Badge tone={tone === "mint" ? "mint" : tone === "warn" ? "warn" : tone === "danger" ? "danger" : "plain"}>
151 {percentLabel(p.marginPercent)}
152 </Badge>
153 )}
154 </td>
155 </tr>
156 );
157 })}
158 </tbody>
159 </table>
160 </div>
161 </Section>
162
163 <Section
164 className="mt-6"
165 title="Workspaces that cost most"
166 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."
167 >
168 {report.topWorkspaces.length === 0 ? (
169 <p className="text-sm text-muted">No costs shared out yet.</p>
170 ) : (
171 <div className="-mx-4 overflow-x-auto sm:-mx-5">
172 <table className="w-full min-w-136 text-sm">
173 <thead>
174 <tr className="border-b border-line text-left text-xs text-muted">
175 <th className="px-4 py-2 font-medium sm:px-5">Workspace</th>
176 <th className="px-4 py-2 text-right font-medium">Cost to g1t</th>
177 <th className="px-4 py-2 text-right font-medium">Given away</th>
178 <th className="px-4 py-2 text-right font-medium">Paid</th>
179 <th className="px-4 py-2 text-right font-medium sm:pr-5">Net</th>
180 </tr>
181 </thead>
182 <tbody>
183 {report.topWorkspaces.map((w) => {
184 const net = w.revenueMicros - w.costMicros;
185 // What was given away on purpose is a budget, not a loss:
186 // red only for what neither paid nor was given.
187 const lost = net + (w.givenMicros ?? 0) < 0;
188 return (
189 <tr key={w.workspace} className="border-b border-line last:border-0">
190 <td className="px-4 py-2.5 sm:px-5">
191 <Link to={`/workspaces/${encodeURIComponent(w.workspace)}#billing`} className="font-mono text-fg hover:underline">
192 {w.workspace}
193 </Link>
194 {w.internal && <span className="ml-2 text-xs text-faint">g1t's own</span>}
195 </td>
196 <td className="tabular px-4 py-2.5 text-right">{usd(w.costMicros)}</td>
197 <td className="tabular px-4 py-2.5 text-right text-fg-soft">{usd(w.givenMicros ?? 0)}</td>
198 <td className="tabular px-4 py-2.5 text-right">{usd(w.revenueMicros)}</td>
199 <td className={`tabular px-4 py-2.5 text-right sm:pr-5 ${lost && !w.internal ? "text-danger" : "text-fg-soft"}`}>{usd(net)}</td>
200 </tr>
201 );
202 })}
203 </tbody>
204 </table>
205 </div>
206 )}
207 {(report.unattributedMicros ?? 0) > 0 && (
208 <p className="mt-3 text-xs text-muted">
209 And {usd(report.unattributedMicros ?? 0, { cents: true })} of running g1t on days no workspace used anything: no one&apos;s, so not above. The
210 workspaces&apos; costs and this add up to the cost in the statement.
211 </p>
212 )}
213 </Section>
214 </main>
215 );
216}
217
218function CapMeter({ label, used, cap, hint }: { label: string; used: number; cap: number; hint: ReactNode }) {
219 const percent = capPercent(used, cap);
220 const tone = cap > 0 && used >= cap ? "fill-danger" : percent >= 75 ? "fill-warn" : "fill-accent";
221 return (
222 <div className="rounded-md border border-line px-4 py-3">
223 <div className="flex flex-wrap items-baseline justify-between gap-2">
224 <span className="text-sm font-medium">{label}</span>
225 <span className="tabular text-sm">
226 {usd(used)} <span className="text-faint">of {cap > 0 ? usd(cap) : "no cap"}</span>
227 </span>
228 </div>
229 {/* Drawn as SVG, its width an attribute: the page's style policy blocks a style attribute. */}
230 <svg
231 viewBox="0 0 100 4"
232 preserveAspectRatio="none"
233 className="mt-2 block h-1.5 w-full"
234 role="meter"
235 aria-valuemin={0}
236 aria-valuemax={100}
237 aria-valuenow={Math.round(percent)}
238 aria-label={label}
239 >
240 <rect x="0" y="0" width="100" height="4" rx="2" className="fill-line" />
241 {percent > 0 && <rect x="0" y="0" width={Math.max(2, percent)} height="4" rx="2" className={tone} />}
242 </svg>
243 <p className="mt-1.5 text-xs text-muted">{hint}</p>
244 </div>
245 );
246}
247
248/**
249 * Where the money went, as a short statement: usage sold and running g1t
250 * (each against what paid for it), what g1t gave away on purpose (a budget,
251 * never a loss), and who was paid.
252 */
253function Statement({ report, floor, range, proposals }: { report: CostsReport; floor: number; range: number; proposals: number }) {
254 const o = report.overall;
255 // Reports from before the statement have only the totals.
256 const given = o.givenMicros ?? 0;
257 const usageCost = o.usageCostMicros ?? o.costMicros - given;
258 const included = o.includedMicros ?? 0;
259 const usageIn = o.usageMicros + included;
260 const usageMargin = o.usageMarginMicros ?? usageIn - usageCost;
261 // Under a cent sold, a percentage says nothing (a few micros against none).
262 const soldSomething = usageIn >= 10_000;
263 const usagePercent = soldSomething && o.usageMarginPercent !== undefined ? o.usageMarginPercent : null;
264 const running = o.runningCostMicros ?? 0;
265 const unmapped = o.unmappedCostMicros ?? 0;
266 // Cloudflare's subscriptions are not on the usage bill: each day's share of its billing cycle.
267 const subscriptions = subscriptionsOver(report.caps.fixedMonthlyMicros, range, o);
268 const paid = whoPaid(o, subscriptions);
269 const moneyIn = o.usageMicros + o.plansMicros;
270 const spent = paid.totalMicros;
271 const net = moneyIn - spent;
272 const givenParts = givenBy(o);
273 const rows: { title: string; note: string; in: number | null; cost: number; result: number | null; tone?: "danger" | "warn" | "muted" }[] = [
274 {
275 title: "Usage sold",
276 note:
277 included > 0
278 ? `What workspaces paid for usage (${usd(o.usageMicros, { cents: true })}) and their plan's included usage (${usd(included, { cents: true })}), against what it cost`
279 : "What workspaces paid for usage, against what that usage cost",
280 in: usageIn,
281 cost: usageCost,
282 result: usageMargin,
283 tone: usageMargin < 0 ? "danger" : undefined,
284 },
285 {
286 title: "Running g1t",
287 note:
288 included > 0
289 ? `Plans (${usd(o.plansMicros, { cents: true })}) less the included usage they paid for, against Workers, D1, KV, Queues and the rest`
290 : "Plans, against Workers, D1, KV, Queues and the rest of the platform",
291 in: o.plansMicros - included,
292 cost: running,
293 result: o.plansMicros - included - running,
294 tone: o.plansMicros - included - running < 0 ? "warn" : undefined,
295 },
296 {
297 title: "Cloudflare subscriptions",
298 note:
299 report.caps.fixedSource === "cloudflare"
300 ? `Fixed, as Cloudflare lists them${(report.caps.fixedItems ?? []).length > 0 ? ` (${(report.caps.fixedItems ?? []).map((i) => i.name).join(", ")})` : ""}; not on the usage bill. Each day is its billing cycle's share of ${usd(report.caps.fixedMonthlyMicros, { cents: true })} a month`
301 : "Fixed, an estimate (CLOUDFLARE_FIXED_MONTHLY_MICROS) until Cloudflare's list is read; not on the usage bill",
302 in: null,
303 cost: subscriptions,
304 result: -subscriptions,
305 tone: "muted",
306 },
307 ...(unmapped > 0
308 ? [
309 {
310 title: "Not mapped",
311 note: "Billed by Cloudflare, charged for by nothing yet",
312 in: null,
313 cost: unmapped,
314 result: -unmapped,
315 tone: "danger" as const,
316 },
317 ]
318 : []),
319 {
320 title: "Given away",
321 note: givenParts.length > 0 ? givenParts.map(([why, micros]) => `${why} ${usd(micros, { cents: true })}`).join(" · ") : "Nothing this range",
322 in: null,
323 cost: given,
324 result: null,
325 tone: "muted",
326 },
327 ];
328 const resultClass = (tone?: string) => (tone === "danger" ? "text-danger" : tone === "warn" ? "text-warn" : tone === "muted" ? "text-muted" : "text-fg");
329 return (
330 <>
331 <div className="mt-5 grid grid-cols-2 gap-3 lg:grid-cols-4">
332 <Stat
333 label="Margin on usage sold"
334 value={percentLabel(usagePercent)}
335 hint={
336 soldSomething
337 ? `Of the price: ${usd(usageIn, { cents: true })} paid for usage that cost ${usd(usageCost, { cents: true })}`
338 : "No usage sold in this range"
339 }
340 tone={marginTone(usagePercent, floor)}
341 />
342 <Stat
343 label="Given away on purpose"
344 value={usd(given, { cents: true })}
345 hint={givenParts.length > 0 ? givenParts.map(([why, micros]) => `${usd(micros, { cents: true })} ${why}`).join(", ") : "Nothing given this range"}
346 />
347 <Stat
348 label="Who g1t paid"
349 value={usd(paid.totalMicros, { cents: true })}
350 hint={`Cloudflare's usage ${usd(paid.cloudflareMicros, { cents: true })} and subscriptions ${usd(paid.subscriptionsMicros, { cents: true })}${paid.modelsMicros > 0 ? `, model providers ${usd(paid.modelsMicros, { cents: true })} (from before agents were retired)` : ""}; the cost in All in`}
351 />
352 <Stat
353 label="Proposals waiting"
354 value={String(proposals)}
355 hint={
356 <>
357 <Link to={costsHref("bill", range) + "#proposals"} className="underline underline-offset-2">
358 On Bill &amp; pricing
359 </Link>
360 {report.fetchedAt ? (
361 <>
362 {" "}
363 · bill read <When at={report.fetchedAt} time />
364 </>
365 ) : (
366 " · the bill has not been read yet"
367 )}
368 </>
369 }
370 tone={proposals > 0 ? "warn" : undefined}
371 />
372 </div>
373 <div className="mt-3 overflow-x-auto rounded-lg border border-line bg-surface">
374 <table className="w-full min-w-xl text-sm">
375 <thead>
376 <tr className="border-b border-line text-left text-xs text-muted">
377 <th className="px-4 py-2 font-medium sm:px-5">
378 {report.since} to {report.until}
379 </th>
380 <th className="px-4 py-2 text-right font-medium">Paid in</th>
381 <th className="px-4 py-2 text-right font-medium">Cost</th>
382 <th className="px-4 py-2 text-right font-medium sm:pr-5">Result</th>
383 </tr>
384 </thead>
385 <tbody>
386 {rows.map((row) => (
387 <tr key={row.title} className="border-b border-line">
388 <td className="px-4 py-2.5 sm:px-5">
389 <span className="text-fg">{row.title}</span>
390 <span className="block text-xs text-faint">{row.note}</span>
391 </td>
392 <td className="tabular px-4 py-2.5 text-right">{row.in == null ? <span className="text-faint">—</span> : usd(row.in, { cents: true })}</td>
393 <td className="tabular px-4 py-2.5 text-right">{usd(row.cost, { cents: true })}</td>
394 <td className={`tabular px-4 py-2.5 text-right sm:pr-5 ${resultClass(row.tone)}`}>
395 {row.result == null ? "a budget" : usd(row.result, { signed: true, cents: true })}
396 </td>
397 </tr>
398 ))}
399 <tr>
400 <td className="px-4 py-2.5 sm:px-5">
401 <span className="font-medium text-fg">All in</span>
402 <span className="block text-xs text-faint">
403 {given > 0
404 ? `${usd(given, { cents: true })} of the cost was given away on purpose; without it, ${usd(net + given, { signed: true, cents: true })}`
405 : "Everything above"}
406 </span>
407 </td>
408 <td className="tabular px-4 py-2.5 text-right font-medium">{usd(moneyIn, { cents: true })}</td>
409 <td className="tabular px-4 py-2.5 text-right font-medium">{usd(spent, { cents: true })}</td>
410 <td className="tabular px-4 py-2.5 text-right font-medium text-fg sm:pr-5">{usd(net, { signed: true, cents: true })}</td>
411 </tr>
412 </tbody>
413 </table>
414 </div>
415 <p className="mt-2 text-xs text-faint">
416 Credits from g1t, {report.since} to {report.until}: {usd(o.creditsGivenMicros ?? 0, { cents: true })} given,{" "}
417 {usd(o.creditsUsedMicros ?? 0, { cents: true })} spent on usage. What promotional and goodwill credit paid for is given away above, never money in;
418 refunds took {usd(o.creditsRefundedMicros ?? 0, { cents: true })} off money in on the days they refund.{" "}
419 <Link to="/credits" className="underline underline-offset-2">
420 Every credit
421 </Link>
422 </p>
423 <dl className="mt-3 grid grid-cols-2 gap-3 rounded-lg border border-line bg-surface px-4 py-3 text-sm sm:px-5">
424 <div>
425 <dt className="text-xs text-muted">Tax collected</dt>
426 <dd className="tabular text-fg">{usd(o.taxCollectedMicros ?? 0, { cents: true })}</dd>
427 <dd className="text-xs text-faint">Owed to tax authorities; never in money in or margin. Filed from Stripe Tax.</dd>
428 </div>
429 <div>
430 <dt className="text-xs text-muted">Card fees passed on</dt>
431 <dd className="tabular text-fg">{usd(o.cardFeesMicros ?? 0, { cents: true })}</dd>
432 <dd className="text-xs text-faint">Only payments from before the fee was folded into prices; not revenue either.</dd>
433 </div>
434 </dl>
435 </>
436 );
437}
438
439function SpendSection({ caps, range, error }: { caps: CostsReport["caps"]; range: number; error: string | null }) {
440 const { rows, totalMicros } = spendRows(caps);
441 const net = caps.revenueMicros - totalMicros;
442 return (
443 <Section
444 className="mt-6"
445 id="spend"
446 title="g1t's own spend"
447 description="What g1t pays for itself this calendar month, today included: accounts on a 100% discount, the trial and open-source pools, free workspaces' overruns, and usage charged without real money behind it. Each is what the work cost g1t, never its price, counted as each charge settled: the price book's cost of sandboxes and builds. Two caps hold it: each 100%-discount account's monthly budget, and a daily breaker on all of it that pauses new compute g1t would pay for: workflows, checks and the merge queue."
448 >
449 <div className="grid gap-3 lg:grid-cols-2">
450 <CapMeter
451 label={`Today, ${caps.day} (UTC)`}
452 used={caps.todayMicros}
453 cap={caps.dailyCapMicros}
454 hint={
455 caps.tripped ? (
456 <span className="text-danger">
457 Breaker open
458 {caps.trippedAt ? (
459 <>
460 {" "}
461 since <When at={caps.trippedAt} time />
462 </>
463 ) : null}
464 : new work g1t pays for waits until 00:00 UTC.
465 </span>
466 ) : caps.liftedBy ? (
467 <>
468 Lifted for today by {caps.liftedBy}
469 {caps.liftNote ? `: “${caps.liftNote}”` : ""}.
470 </>
471 ) : (
472 "Paying workspaces on a live card are never paused. PLATFORM_DAILY_SPEND_CAP_MICROS."
473 )
474 }
475 />
476 {caps.comped.map((b) => (
477 <CapMeter
478 key={b.account}
479 label={`${b.name}, ${caps.month} (100% discount)`}
480 used={b.usedMicros}
481 cap={b.ceilingMicros}
482 hint={
483 <>
484 {b.ceilingMicros > 0 && b.usedMicros >= b.ceilingMicros ? <span className="text-danger">Used up: new work on it is refused. </span> : null}
485 {b.defaultCeiling ? "The default budget (COMPED_MONTHLY_CEILING_MICROS)" : "Its own budget, in its terms"}. Raise it on{" "}
486 <Link to={`/workspaces/${encodeURIComponent(b.name)}#billing`} className="underline underline-offset-2">
487 its account
488 </Link>
489 : Terms, Limit.
490 </>
491 }
492 />
493 ))}
494 </div>
495 {(caps.tripped || error) && (
496 <form method="post" action="#spend" className="mt-4 flex flex-col gap-2 sm:flex-row sm:items-end">
497 <input type="hidden" name="intent" value="lift" />
498 <Field label="Why lift it" hint="Recorded in the audit log.">
499 <Input name="note" required minLength={5} maxLength={500} placeholder="e.g. Launch day; watching it" className="sm:min-w-[24rem]" />
500 </Field>
501 <Button type="submit" variant="danger">
502 Lift for today
503 </Button>
504 </form>
505 )}
506 {error && (
507 <div className="mt-3">
508 <Notice tone="error">{error}</Notice>
509 </div>
510 )}
511 <div className="-mx-4 mt-5 overflow-x-auto sm:-mx-5">
512 <table className="w-full min-w-136 text-sm">
513 <thead>
514 <tr className="border-b border-line text-left text-xs text-muted">
515 <th className="px-4 py-2 font-medium sm:px-5">{caps.month}, so far</th>
516 <th className="px-4 py-2 text-right font-medium sm:pr-5">g1t paid</th>
517 </tr>
518 </thead>
519 <tbody>
520 {rows.map((row) => (
521 <tr key={row.key} className="border-b border-line align-top">
522 <td className="px-4 py-2.5 sm:px-5">
523 {row.title}
524 <span className="block text-xs text-faint">{row.note}</span>
525 </td>
526 <td className="tabular px-4 py-2.5 text-right sm:pr-5">{usd(row.micros)}</td>
527 </tr>
528 ))}
529 <tr className="border-b border-line font-medium">
530 <td className="px-4 py-2.5 sm:px-5">All of it</td>
531 <td className="tabular px-4 py-2.5 text-right sm:pr-5">{usd(totalMicros)}</td>
532 </tr>
533 <tr className="border-b border-line">
534 <td className="px-4 py-2.5 sm:px-5">
535 Money in
536 <span className="block text-xs text-faint">Usage paid for with real money and the plan, as last reconciled</span>
537 </td>
538 <td className="tabular px-4 py-2.5 text-right sm:pr-5">{usd(caps.revenueMicros)}</td>
539 </tr>
540 <tr>
541 <td className="px-4 py-2.5 sm:px-5">Money in less what g1t paid</td>
542 <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>
543 </tr>
544 </tbody>
545 </table>
546 </div>
547 <p className="mt-3 text-xs text-muted">
548 Why this differs from the statement above: this is {caps.month} so far, the statement the last {range} days. The statement takes Cloudflare&apos;s bill
549 as the cost of what Cloudflare runs, so sandboxes and builds inside Cloudflare&apos;s included usage cost nothing there, and here what the price book
550 says they cost. Cloudflare&apos;s subscriptions are counted the same way in both: each day its billing cycle&apos;s share of the month&apos;s price.
551 {(caps.resetMicros ?? 0) > 0 && (
552 <>
553 {" "}
554 And {usd(caps.resetMicros ?? 0, { cents: true })} of this was spent on {(caps.resetWorkspaces ?? []).join(", ")} before a testing reset wiped
555 {(caps.resetWorkspaces ?? []).length === 1 ? " its" : " their"} billing: still g1t&apos;s spend, but the statement has it only where the reset kept
556 it, as given away.
557 </>
558 )}
559 </p>
560 </Section>
561 );
562}
563
564/**
565 * The platform pause and the hourly usage watch (billing's platform.rs;
566 * docs.g1t.sh/guides/deploy-to-cloudflare/#spend-guardrails): four levels
567 * staff can pause across g1t, what Cloudflare counted in the last hour
568 * against each threshold, and the last day's breaches.
569 */
570function PlatformSection({ guard, unavailable, failed }: { guard: PlatformGuard | null; unavailable: string | null; failed: CostsActionResult | null }) {
571 return (
572 <Section
573 className="mt-6"
574 id="platform"
575 title="Platform pause"
576 description="What Cloudflare counted for all of g1t, read at a quarter past each hour: Workers, D1, Queues, Durable Objects, KV and Artifacts, each against an hourly threshold (PLATFORM_HOURLY_* in billing's wrangler.jsonc). A breach emails staff once per metric every 6 hours. Five times a threshold pauses what that metric feeds, of the levels AUTO_PAUSE names. Any level can be paused here by hand; every service sees a change within 30 seconds."
577 >
578 {!guard ? (
579 <Notice tone="warn">Billing did not answer for the platform pause: {unavailable}</Notice>
580 ) : (
581 <>
582 {!guard.can_read && (
583 <div className="mb-4">
584 <Notice tone="warn">
585 Billing has no Cloudflare token with Account Analytics Read, so the hourly watch reads nothing. The pause still works.
586 </Notice>
587 </div>
588 )}
589 {(guard.blind ?? []).length > 0 && (
590 <div className="mb-4">
591 <Notice tone="warn">
592 The watcher can&apos;t see: {(guard.blind ?? []).map((b) => b.key).join(", ")}
593 {guard.last_run ? ` (the run for ${guard.last_run})` : ""}. Their metrics are not watched until it is fixed; three runs in a row email staff.
594 <ul className="mt-2 space-y-1 text-xs">
595 {(guard.blind ?? []).map((b) => (
596 <li key={b.key}>
597 <code>{b.dataset}</code> ({b.key}): {b.error}
598 </li>
599 ))}
600 </ul>
601 </Notice>
602 </div>
603 )}
604 {guard.empty && (
605 <div className="mb-4">
606 <Notice tone="warn">
607 Every dataset answered with no rows{guard.last_run ? ` in the run for ${guard.last_run}` : ""}, the month so far included: likely the wrong
608 account or a token that cannot see its analytics. The watcher sees nothing.
609 </Notice>
610 </div>
611 )}
612 {failed?.section === "platform" && (
613 <div className="mb-4">
614 <Notice tone="error">{failed.error}</Notice>
615 </div>
616 )}
617 <div className="grid gap-3 lg:grid-cols-2">
618 {PAUSE_LEVELS.map(({ level, title, stops }) => {
619 const state = guard.levels.find((l) => l.level === level);
620 const paused = state?.paused ?? false;
621 const error = failed?.section === `pause-${level}` ? failed.error : null;
622 return (
623 <div key={level} className={`rounded-lg border p-4 ${paused ? "border-danger/50 bg-danger/8" : "border-line"}`}>
624 <div className="flex items-center justify-between gap-2">
625 <span className="font-medium">{title}</span>
626 {paused ? <Badge tone="danger">Paused</Badge> : <Badge tone="mint">Running</Badge>}
627 </div>
628 <p className="mt-1 text-xs text-muted">{stops}</p>
629 {state?.set_at && (
630 <p className="mt-2 text-xs text-faint">
631 {paused ? "Paused" : "Resumed"} <When at={state.set_at} time /> by {state.auto ? "the usage watcher" : state.set_by}
632 {state.note ? `: “${state.note}”` : ""}
633 </p>
634 )}
635 <form method="post" action="#platform" className="mt-3 flex flex-col gap-2 sm:flex-row sm:items-end">
636 <input type="hidden" name="intent" value={paused ? "resume" : "pause"} />
637 <input type="hidden" name="level" value={level} />
638 <Field label={paused ? "Why resume it" : "Why pause it"} hint="Recorded in the audit log.">
639 <Input
640 name="note"
641 required
642 minLength={5}
643 maxLength={500}
644 placeholder={paused ? "e.g. Fixed the loop in search" : "e.g. KV lists runaway"}
645 />
646 </Field>
647 <Button type="submit" variant={paused ? "primary" : "danger"}>
648 {paused ? "Resume" : "Pause"}
649 </Button>
650 </form>
651 {error && (
652 <div className="mt-2">
653 <Notice tone="error">{error}</Notice>
654 </div>
655 )}
656 </div>
657 );
658 })}
659 </div>
660 <p className="mt-3 text-xs text-muted">
661 A severe breach may pause: {guard.auto_pause.length ? guard.auto_pause.join(", ") : "nothing (AUTO_PAUSE is empty)"}.
662 </p>
663
664 <h3 className="mt-6 text-sm font-medium">Breaches in the last 24 hours</h3>
665 {guard.breaches.length === 0 ? (
666 <p className="mt-2 text-sm text-muted">None.</p>
667 ) : (
668 <ul className="mt-2 space-y-2 text-sm">
669 {guard.breaches.map((b) => (
670 <li key={b.id} className="rounded-md border border-line px-3 py-2">
671 <div className="flex flex-wrap items-center gap-2">
672 <Badge tone={b.severe ? "danger" : "warn"}>{b.severe ? "Severe" : b.rule === "spike" ? "Spike" : "Over threshold"}</Badge>
673 <span className="text-xs text-faint">
674 <When at={b.opened_at} time />
675 {b.emailed_at ? ", emailed" : ""}
676 </span>
677 </div>
678 <p className="mt-1 text-fg-soft">{b.detail}</p>
679 </li>
680 ))}
681 </ul>
682 )}
683
684 <UsageTable title={guard.hour ? `The hour from ${guard.hour}` : "The last hour"} metrics={guard.last_hour} hourly />
685 <UsageTable title={`${guard.month}, so far`} metrics={guard.month_to_date} hourly={false} />
686 <p className="mt-3 text-xs text-muted">
687 Ids are Cloudflare&apos;s. <code>node scripts/ops/platform-usage.mjs</code> names them and shows the last 24 hours per script, queue, database and
688 namespace.
689 </p>
690 </>
691 )}
692 </Section>
693 );
694}
695
696function UsageTable({ title, metrics, hourly }: { title: string; metrics: PlatformMetric[]; hourly: boolean }) {
697 return (
698 <>
699 <h3 className="mt-6 text-sm font-medium">{title}</h3>
700 {metrics.length === 0 ? (
701 <p className="mt-2 text-sm text-muted">Nothing read yet.</p>
702 ) : (
703 <div className="-mx-4 mt-2 overflow-x-auto sm:-mx-5">
704 <table className="w-full min-w-xl text-sm">
705 <thead>
706 <tr className="border-b border-line text-left text-xs text-muted">
707 <th className="px-4 py-2 font-medium sm:px-5">Metric</th>
708 <th className="px-4 py-2 text-right font-medium">Count</th>
709 {hourly && <th className="px-4 py-2 text-right font-medium">Of threshold</th>}
710 <th className="px-4 py-2 font-medium sm:pr-5">Most from</th>
711 </tr>
712 </thead>
713 <tbody>
714 {metrics.map((m) => {
715 const share = thresholdShare(m.value, m.threshold);
716 return (
717 <tr key={m.metric} className="border-b border-line last:border-0">
718 <td className="px-4 py-2.5 sm:px-5">{m.title}</td>
719 <td className="tabular px-4 py-2.5 text-right">{count(m.value)}</td>
720 {hourly && (
721 <td className={`tabular px-4 py-2.5 text-right ${share != null && share > 100 ? "text-danger" : "text-muted"}`}>
722 {share == null ? "—" : `${share}% of ${count(m.threshold)}`}
723 </td>
724 )}
725 <td className="max-w-[16rem] truncate px-4 py-2.5 text-xs text-faint sm:pr-5">
726 {m.top_name ? `${m.top_name} (${count(m.top_value ?? 0)})` : "—"}
727 </td>
728 </tr>
729 );
730 })}
731 </tbody>
732 </table>
733 </div>
734 )}
735 </>
736 );
737}