Skip to content

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

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