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

906 lines44,220 bytesCodeBlame

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.

Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays1import type { ReactNode } from "react";
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running2import { Form, Link, data, redirect, useNavigation } from "react-router";
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily3
4import type { CostsReport, PriceProposal } from "@g1t/contracts";
5
6import type { Route } from "./+types/costs";
7import { DaysChart } from "~/components/costs";
8import { Badge, Button, EmptyState, Field, Input, Notice, PageHeader, Section, Stat, When } from "~/components/ui";
9import {
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays10 capPercent,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily11 countLabel,
12 daySeries,
13 driftLabel,
14 marginTone,
15 parseBucket,
16 parseCostSettings,
17 parseMapping,
18 parseRange,
19 percentLabel,
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays20 spendRows,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily21 unitDollars,
22} from "~/lib/costs";
23import { dollarsField, usd } from "~/lib/money";
24import { admin } from "~/lib/services.server";
25import { settle } from "~/lib/settle";
26import { requireStaff } from "~/lib/staff";
27
28export const meta: Route.MetaFunction = () => [{ title: "Costs & margin · sudo" }, { name: "robots", content: "noindex, nofollow" }];
29
30const DONE: Record<string, string> = {
sudo: the costs run button is named for what it does, the whole nightly analysis31 run: "Ran the analysis: read Cloudflare's bill, reconciled the last 31 days and checked the alerts. The figures below are fresh.",
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily32 approved: "Approved. A fall applies now; a rise after the notice period, and owners on the plan are emailed.",
33 rejected: "Rejected, with the note kept for whoever measures it next.",
34 settings: "Guardrails saved. They apply from the next run.",
35 mapping: "Mapping saved. It applies from the next run; read the bill now to see it.",
36 removed: "Mapping removed.",
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays37 lifted: "Breaker lifted for the rest of today (UTC). Hosted-model runs start again; it is recorded in the audit log.",
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily38};
39
40const RANGES = [7, 30, 90];
41
42export async function loader({ request, context }: Route.LoaderArgs) {
43 requireStaff(context);
44 const url = new URL(request.url);
45 const range = parseRange(url.searchParams.get("days"));
46 const report = await settle(admin.costs(range));
47 const done = url.searchParams.get("done");
48 return {
49 range,
50 bucket: url.searchParams.get("product"),
51 report: report.ok ? report.value : null,
52 error: report.ok ? null : report.error,
53 done: done ? (DONE[done] ?? null) : null,
54 };
55}
56
57type ActionResult = { error: string; section: string; values?: Record<string, string> };
58
59export async function action({ request, context }: Route.ActionArgs) {
60 const staff = requireStaff(context);
61 const form = await request.formData();
62 const intent = String(form.get("intent") ?? "");
63 const fail = (section: string, error: string) =>
64 data<ActionResult>({ error, section, values: Object.fromEntries([...form.entries()].map(([k, v]) => [k, String(v)])) }, { status: 422 });
65 const back = (done: string, hash = "") => {
66 const url = new URL(request.url);
67 url.searchParams.set("done", done);
68 return redirect(`${url.pathname}${url.search}${hash}`);
69 };
70 if (intent === "run") {
71 const run = await settle(admin.runCosts(staff.email));
72 if (!run.ok) return fail("run", `Billing did not answer: ${run.error}`);
73 if (!run.value.ok) return fail("run", run.value.error.message);
74 if (run.value.value.problems.length > 0) return fail("run", run.value.value.problems.join(" "));
75 throw back("run");
76 }
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays77 if (intent === "lift") {
78 const note = String(form.get("note") ?? "").trim().slice(0, 500);
79 if (note.length < 5) return fail("lift", "Say why it is lifted, for whoever looks next.");
80 const result = await settle(admin.liftBreaker(note, staff.email));
81 if (!result.ok) return fail("lift", `Billing did not answer: ${result.error}`);
82 if (!result.value.ok) return fail("lift", result.value.error.message);
83 throw back("lifted", "#spend");
84 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily85 if (intent === "decide") {
86 const id = String(form.get("id") ?? "");
87 const decision = form.get("decision") === "approve" ? "approve" : "reject";
88 const note = String(form.get("note") ?? "").trim().slice(0, 500);
89 if (decision === "reject" && note.length < 5) return fail(`proposal-${id}`, "Say why it is rejected, for whoever measures it next.");
90 const result = await admin.decideProposal(id, decision, note, staff.email);
91 if (!result.ok) return fail(`proposal-${id}`, result.error.message);
92 throw back(decision === "approve" ? "approved" : "rejected", "#proposals");
93 }
94 if (intent === "settings") {
95 const parsed = parseCostSettings(form);
96 if (!parsed.ok) return fail("settings", parsed.error);
97 const result = await admin.setCostSettings(parsed.value, staff.email);
98 if (!result.ok) return fail("settings", result.error.message);
99 throw back("settings", "#guardrails");
100 }
101 if (intent === "mapping") {
102 const parsed = parseMapping(form);
103 if (!parsed.ok) return fail("mapping", parsed.error);
104 const result = await admin.setCostMapping(parsed.value, staff.email);
105 if (!result.ok) return fail("mapping", result.error.message);
106 throw back(parsed.value.remove ? "removed" : "mapping", "#mappings");
107 }
108 return fail("run", "Unknown change.");
109}
110
111function chip(active: boolean) {
112 return `inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs whitespace-nowrap transition-colors ${
113 active ? "border-merged/50 bg-merged/10 text-merged" : "border-line text-muted hover:border-line-strong hover:text-fg"
114 }`;
115}
116
117function href(range: number, bucket: string | null) {
118 const params = new URLSearchParams();
119 if (range !== 30) params.set("days", String(range));
120 if (bucket) params.set("product", bucket);
121 const query = params.toString();
122 return query ? `/costs?${query}` : "/costs";
123}
124
125export default function Costs({ loaderData, actionData }: Route.ComponentProps) {
126 const { range, report, error, done } = loaderData;
127 const failed = actionData && "error" in actionData ? (actionData as ActionResult) : null;
128 if (!report) {
129 return (
130 <main className="mx-auto max-w-6xl px-4 py-8 sm:py-10">
131 <PageHeader title="Costs & margin" />
132 <div className="mt-5">
133 <Notice tone="warn">Billing did not answer for costs: {error}</Notice>
134 </div>
135 </main>
136 );
137 }
138 const bucket = parseBucket(loaderData.bucket, report.products.map((p) => p.bucket));
139 const product = report.products.find((p) => p.bucket === bucket) ?? null;
140 const series = daySeries(report.days, report.since, report.until, bucket);
141 const floor = report.settings.marginFloorPercent;
142 const open = report.proposals.filter((p) => p.status === "open");
143 // Margin under the floor at the top; drift and leaks are in their own table, workspaces on Reach out.
144 const banner = report.alerts.filter((a) => a.kind === "overall" || a.kind === "margin");
145 const elsewhere = report.alerts.length - banner.length;
146 return (
147 <main className="mx-auto max-w-6xl px-4 py-8 sm:py-10">
148 <PageHeader
149 title="Costs & margin"
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running150 description="What g1t cost to run (Cloudflare's bill and the model providers') against what workspaces paid. Prices are cost plus 20%; this is where that is checked against the bill, where drift and leaks show, and where price changes wait for a decision."
151 actions={<RunButton />}
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily152 />
153 <nav aria-label="Range" className="mt-5 flex flex-wrap gap-2">
154 {RANGES.map((days) => (
155 <Link key={days} to={href(days, bucket)} aria-current={days === range ? "page" : undefined} className={chip(days === range)}>
156 Last {days} days
157 </Link>
158 ))}
159 </nav>
160 <div className="mt-5 space-y-3 empty:hidden">
161 {done && <Notice tone="ok">{done}</Notice>}
162 {failed?.section === "run" && <Notice tone="error">{failed.error}</Notice>}
163 {!report.configured && (
164 <Notice tone="warn">
165 Billing cannot read Cloudflare's bill: set the <code className="font-mono">CLOUDFLARE_BILLING_TOKEN</code> secret on g1t-billing (Account:
166 Billing Read and Account Analytics Read). Until then the figures are g1t's own, and Cloudflare's cost shows as nothing.
167 </Notice>
168 )}
169 {banner.map((alert) => (
170 <Notice key={alert.id} tone="error">
171 {alert.detail} <span className="text-faint">Since {alert.since}.</span>
172 </Notice>
173 ))}
174 {elsewhere > 0 && (
175 <p className="text-xs text-muted">
176 {elsewhere} more open: {" "}
177 <a href="#drift" className="underline underline-offset-2">
178 drift and leaks
179 </a>{" "}
180 below, and workspaces that cost more than they pay on{" "}
181 <Link to="/reach-out?kind=cost_over_revenue" className="underline underline-offset-2">
182 Reach out
183 </Link>
184 .
185 </p>
186 )}
187 </div>
188
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running189 <Statement report={report} floor={floor} range={range} proposals={open.length} />
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily190
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays191 <SpendSection caps={report.caps} error={failed?.section === "lift" ? failed.error : null} />
192
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily193 <Section
194 className="mt-6"
195 title={product ? `${product.title}, by day` : "By day"}
196 description={
197 product
198 ? "What customers were charged for it at price, before included usage and pools paid part, against what it cost."
199 : "Money in (usage paid for and the plan) against every cost. Pick a product to see its own."
200 }
201 >
202 <nav aria-label="Product" className="mb-4 flex flex-wrap gap-2">
203 <Link to={href(range, null)} aria-current={!bucket ? "page" : undefined} className={chip(!bucket)}>
204 All of g1t
205 </Link>
206 {report.products.map((p) => (
207 <Link key={p.bucket} to={href(range, p.bucket)} aria-current={p.bucket === bucket ? "page" : undefined} className={chip(p.bucket === bucket)}>
208 {p.title}
209 </Link>
210 ))}
211 </nav>
212 <DaysChart
213 days={series}
214 floor={floor}
215 revenueLabel={product ? "Charged at price" : "Money in"}
216 label={product ? `${product.title}: charged against Cloudflare cost by day` : "Money in against Cloudflare cost by day"}
217 />
218 </Section>
219
220 <Section className="mt-6" title="By product" description={`Each of g1t's products over the last ${range} days. The floor is ${floor}%.`}>
221 <div className="-mx-4 overflow-x-auto sm:-mx-5">
222 <table className="w-full min-w-[44rem] text-sm">
223 <thead>
224 <tr className="border-b border-line text-left text-xs text-muted">
225 <th className="px-4 py-2 font-medium sm:px-5">Product</th>
226 <th className="px-4 py-2 text-right font-medium">Charged at price</th>
227 <th className="px-4 py-2 text-right font-medium">Cost</th>
228 <th className="px-4 py-2 text-right font-medium">Price book said</th>
229 <th className="px-4 py-2 text-right font-medium">Margin</th>
230 <th className="px-4 py-2 text-right font-medium sm:pr-5">%</th>
231 </tr>
232 </thead>
233 <tbody>
234 {report.products.map((p) => {
235 const tone = p.overhead ? undefined : marginTone(p.marginPercent, floor);
236 return (
237 <tr key={p.bucket} className="border-b border-line last:border-0">
238 <td className="px-4 py-2.5 sm:px-5">
239 <Link to={href(range, p.bucket)} className="text-fg hover:underline">
240 {p.title}
241 </Link>
242 <span className="block text-xs text-faint">
243 {p.overhead ? "Paid by the plan" : p.costSource === "ledger" ? "Cost from the gateway, not Cloudflare" : p.bucket}
244 </span>
245 </td>
246 <td className="tabular px-4 py-2.5 text-right">{usd(p.valueMicros)}</td>
247 <td className="tabular px-4 py-2.5 text-right">{usd(p.costMicros)}</td>
248 <td className="tabular px-4 py-2.5 text-right text-muted">{usd(p.ownCostMicros)}</td>
249 <td className={`tabular px-4 py-2.5 text-right ${p.marginMicros < 0 && !p.overhead ? "text-danger" : ""}`}>{usd(p.marginMicros)}</td>
250 <td className="px-4 py-2.5 text-right sm:pr-5">
251 {p.overhead ? (
252 <span className="text-faint">—</span>
253 ) : (
254 <Badge tone={tone === "mint" ? "mint" : tone === "warn" ? "warn" : tone === "danger" ? "danger" : "plain"}>{percentLabel(p.marginPercent)}</Badge>
255 )}
256 </td>
257 </tr>
258 );
259 })}
260 </tbody>
261 </table>
262 </div>
263 </Section>
264
265 <Section
266 className="mt-6"
267 id="drift"
268 title="Drift"
269 description="Over the last 7 days: counts g1t and Cloudflare disagree on past a mapping's threshold, a bill far from the price book's cost of the same usage, and leaks (cost nothing charges for, or a Cloudflare meter no one mapped)."
270 >
271 {report.drift.length === 0 ? (
272 <p className="text-sm text-muted">Nothing has drifted.</p>
273 ) : (
274 <div className="-mx-4 overflow-x-auto sm:-mx-5">
275 <table className="w-full min-w-[44rem] text-sm">
276 <thead>
277 <tr className="border-b border-line text-left text-xs text-muted">
278 <th className="px-4 py-2 font-medium sm:px-5">Product</th>
279 <th className="px-4 py-2 font-medium">Kind</th>
280 <th className="px-4 py-2 text-right font-medium">g1t</th>
281 <th className="px-4 py-2 text-right font-medium">Cloudflare</th>
282 <th className="px-4 py-2 text-right font-medium sm:pr-5">Delta</th>
283 </tr>
284 </thead>
285 <tbody>
286 {report.drift.map((d) => {
287 const money = d.kind !== "count";
288 return (
289 <tr key={`${d.bucket}-${d.kind}`} className="border-b border-line align-top last:border-0">
290 <td className="px-4 py-2.5 sm:px-5">
291 {d.title}
292 <span className="mt-0.5 block max-w-md text-xs text-faint">{d.detail.replace(`${d.title}: `, "")}</span>
293 </td>
294 <td className="px-4 py-2.5">
295 <Badge tone={d.kind === "leak" ? "danger" : "warn"}>{driftLabel(d.kind)}</Badge>
296 </td>
297 <td className="tabular px-4 py-2.5 text-right">{money ? usd(d.ours) : countLabel(d.ours)}</td>
298 <td className="tabular px-4 py-2.5 text-right">{money ? usd(d.cloudflare) : countLabel(d.cloudflare)}</td>
299 <td className="tabular px-4 py-2.5 text-right sm:pr-5">{percentLabel(d.deltaPercent, { signed: true })}</td>
300 </tr>
301 );
302 })}
303 </tbody>
304 </table>
305 </div>
306 )}
307 </Section>
308
309 <Section
310 className="mt-6"
311 id="proposals"
312 title="Price proposals"
313 description={`What the keeper and the reconciler measured. Moves within ${report.settings.autoApplyPercent}% apply themselves${report.settings.autoApply ? "" : " (off now)"}; larger ones wait here. A fall applies at once; a rise after ${report.settings.noticeDays} days' notice, and a monthly meter's from the start of the next month.`}
314 >
315 {report.proposals.length === 0 ? (
316 <p className="text-sm text-muted">No proposals yet.</p>
317 ) : (
318 <ul className="space-y-3">
319 {report.proposals.map((p) => (
320 <ProposalRow key={p.id} proposal={p} error={failed?.section === `proposal-${p.id}` ? failed.error : null} />
321 ))}
322 </ul>
323 )}
324 </Section>
325
326 <Section className="mt-6" title="Price versions" description="Every price as it was and will be. A version never changes once written; each charge records the version it was made at.">
327 <div className="-mx-4 overflow-x-auto sm:-mx-5">
328 <table className="w-full min-w-[44rem] text-sm">
329 <thead>
330 <tr className="border-b border-line text-left text-xs text-muted">
331 <th className="px-4 py-2 font-medium sm:px-5">Meter</th>
332 <th className="px-4 py-2 text-right font-medium">Cost</th>
333 <th className="px-4 py-2 text-right font-medium">Price</th>
334 <th className="px-4 py-2 font-medium">From</th>
335 <th className="px-4 py-2 font-medium sm:pr-5">Why</th>
336 </tr>
337 </thead>
338 <tbody>
339 {report.versions.map((v) => (
340 <tr key={v.id} className="border-b border-line align-top last:border-0">
341 <td className="px-4 py-2.5 sm:px-5">
342 <span className="font-mono text-xs">{v.meter}</span> <span className="text-faint">v{v.version}</span>
343 {!v.appliedAt && (
344 <span className="ml-2">
345 <Badge tone="info">Coming</Badge>
346 </span>
347 )}
348 </td>
349 <td className="tabular px-4 py-2.5 text-right">{unitDollars(v.costMicros)}</td>
350 <td className="tabular px-4 py-2.5 text-right">{unitDollars(v.priceMicros)}</td>
351 <td className="px-4 py-2.5 text-xs text-muted">
352 <When at={v.effectiveAt} />
353 </td>
354 <td className="px-4 py-2.5 text-xs text-faint sm:pr-5">
355 {v.reason} · {v.createdBy}
356 </td>
357 </tr>
358 ))}
359 </tbody>
360 </table>
361 </div>
362 </Section>
363
364 <Section
365 className="mt-6"
366 title="Workspaces that cost most"
367 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."
368 >
369 {report.topWorkspaces.length === 0 ? (
370 <p className="text-sm text-muted">No costs shared out yet.</p>
371 ) : (
372 <div className="-mx-4 overflow-x-auto sm:-mx-5">
373 <table className="w-full min-w-[34rem] text-sm">
374 <thead>
375 <tr className="border-b border-line text-left text-xs text-muted">
376 <th className="px-4 py-2 font-medium sm:px-5">Workspace</th>
377 <th className="px-4 py-2 text-right font-medium">Cost to g1t</th>
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it378 <th className="px-4 py-2 text-right font-medium">Given away</th>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily379 <th className="px-4 py-2 text-right font-medium">Paid</th>
380 <th className="px-4 py-2 text-right font-medium sm:pr-5">Net</th>
381 </tr>
382 </thead>
383 <tbody>
384 {report.topWorkspaces.map((w) => {
385 const net = w.revenueMicros - w.costMicros;
386 return (
387 <tr key={w.workspace} className="border-b border-line last:border-0">
388 <td className="px-4 py-2.5 sm:px-5">
389 <Link to={`/workspaces/${encodeURIComponent(w.workspace)}#billing`} className="font-mono text-fg hover:underline">
390 {w.workspace}
391 </Link>
392 {w.internal && <span className="ml-2 text-xs text-faint">g1t's own</span>}
393 </td>
394 <td className="tabular px-4 py-2.5 text-right">{usd(w.costMicros)}</td>
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it395 <td className="tabular px-4 py-2.5 text-right text-fg-soft">{usd(w.givenMicros ?? 0)}</td>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily396 <td className="tabular px-4 py-2.5 text-right">{usd(w.revenueMicros)}</td>
397 <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>
398 </tr>
399 );
400 })}
401 </tbody>
402 </table>
403 </div>
404 )}
405 </Section>
406
407 <Section
408 className="mt-6"
409 title="Cloudflare's lines"
410 description="Every meter Cloudflare billed or counted in the range, as it named it, and which of g1t's products it is a cost of. Not mapped means no one decided what pays for it."
411 >
412 {report.lines.length === 0 ? (
sudo: the costs run button is named for what it does, the whole nightly analysis413 <EmptyState title="No lines yet">The bill is read once a day at 04:17 UTC, or now with “Run the analysis now”.</EmptyState>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily414 ) : (
415 <div className="-mx-4 overflow-x-auto sm:-mx-5">
416 <table className="w-full min-w-[48rem] text-sm">
417 <thead>
418 <tr className="border-b border-line text-left text-xs text-muted">
419 <th className="px-4 py-2 font-medium sm:px-5">Product / meter</th>
420 <th className="px-4 py-2 text-right font-medium">Quantity</th>
421 <th className="px-4 py-2 text-right font-medium">Cost</th>
422 <th className="px-4 py-2 font-medium sm:pr-5">Cost of</th>
423 </tr>
424 </thead>
425 <tbody>
426 {report.lines.map((l) => (
427 <tr key={`${l.source}-${l.product}-${l.meter}`} className="border-b border-line align-top last:border-0">
428 <td className="px-4 py-2.5 sm:px-5">
429 <span className="font-mono text-xs">
430 {l.product} / {l.meter}
431 </span>
432 <span className="block text-xs text-faint">
433 {l.rawName}
434 {l.source === "artifacts_events" ? " (Artifacts' own count)" : ""}
435 </span>
436 </td>
437 <td className="tabular px-4 py-2.5 text-right">
438 {countLabel(l.quantity)} <span className="text-xs text-faint">{l.unit}</span>
439 </td>
440 <td className="tabular px-4 py-2.5 text-right">{usd(l.costMicros)}</td>
441 <td className="px-4 py-2.5 sm:pr-5">{l.bucket ? <span className="text-fg-soft">{l.bucket}</span> : <Badge tone="danger">Not mapped</Badge>}</td>
442 </tr>
443 ))}
444 </tbody>
445 </table>
446 </div>
447 )}
448 </Section>
449
450 <Section
451 className="mt-6"
452 id="mappings"
453 title="Mappings"
454 description="Which of g1t's products each Cloudflare meter is a cost of. The longest matching meter prefix wins; * is the rest of the product. A price meter is measured from it; an own meter is g1t's count to compare; scaling prices one of g1t's units at as many of Cloudflare's as it took."
455 >
456 <MappingsTable report={report} />
457 <form method="post" action="#mappings" className="mt-5 grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
458 <input type="hidden" name="intent" value="mapping" />
459 <Field label="Cloudflare product">
460 <Input name="product" placeholder="artifacts" defaultValue={failed?.section === "mapping" ? failed.values?.product : ""} required />
461 </Field>
462 <Field label="Meter prefix" hint="Or * for all of it">
463 <Input name="meter" placeholder="*" defaultValue={failed?.section === "mapping" ? failed.values?.meter : ""} />
464 </Field>
465 <Field label="g1t product">
466 <Input name="bucket" placeholder="git" defaultValue={failed?.section === "mapping" ? failed.values?.bucket : ""} />
467 </Field>
468 <Field label="Drift threshold, %">
469 <Input name="driftPercent" inputMode="decimal" placeholder="10" defaultValue={failed?.section === "mapping" ? failed.values?.driftPercent : ""} />
470 </Field>
471 <Field label="Price meter" hint="Optional">
472 <Input name="priceMeter" placeholder="git_operations" defaultValue={failed?.section === "mapping" ? failed.values?.priceMeter : ""} />
473 </Field>
474 <Field label="Own meter" hint="Optional">
475 <Input name="ownMeter" placeholder="git_operations" defaultValue={failed?.section === "mapping" ? failed.values?.ownMeter : ""} />
476 </Field>
477 <Field label="Note">
478 <Input name="note" defaultValue={failed?.section === "mapping" ? failed.values?.note : ""} />
479 </Field>
480 <div className="flex flex-col justify-end gap-2">
481 <label className="flex items-center gap-2 text-sm text-muted">
482 <input type="checkbox" name="scaleToOwn" className="accent-[var(--g1t-merged)]" /> Scale to g1t's count
483 </label>
484 <div className="flex gap-2">
485 <Button type="submit">Save mapping</Button>
486 <Button type="submit" name="remove" value="1" variant="danger">
487 Remove
488 </Button>
489 </div>
490 </div>
491 {failed?.section === "mapping" && (
492 <div className="sm:col-span-2 lg:col-span-4">
493 <Notice tone="error">{failed.error}</Notice>
494 </div>
495 )}
496 </form>
497 </Section>
498
499 <Section className="mt-6" id="guardrails" title="Guardrails" description="What may change prices without a person, and when staff hear about margin.">
500 <form method="post" action="#guardrails" className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
501 <input type="hidden" name="intent" value="settings" />
502 <Field label="Apply small moves on their own">
503 <label className="flex h-[38px] items-center gap-2 text-sm text-fg-soft">
504 <input type="checkbox" name="autoApply" defaultChecked={report.settings.autoApply} className="accent-[var(--g1t-merged)]" /> On
505 </label>
506 </Field>
507 <Field label="Up to, either way, %">
508 <Input name="autoApplyPercent" inputMode="decimal" defaultValue={String(report.settings.autoApplyPercent)} />
509 </Field>
510 <Field label="Notice for a rise, days">
511 <Input name="noticeDays" inputMode="numeric" defaultValue={String(report.settings.noticeDays)} />
512 </Field>
513 <Field label="Margin floor, %">
514 <Input name="marginFloorPercent" inputMode="decimal" defaultValue={String(report.settings.marginFloorPercent)} />
515 </Field>
516 <Field label="Alert after, days in a row">
517 <Input name="alertDays" inputMode="numeric" defaultValue={String(report.settings.alertDays)} />
518 </Field>
519 <Field label="Ignore days costing under, $">
520 <Input name="minDailyCost" inputMode="decimal" defaultValue={dollarsField(report.settings.minDailyCostMicros)} />
521 </Field>
522 <Field label="Flag a workspace costing more than its revenue ×">
523 <Input name="anomalyFactor" inputMode="decimal" defaultValue={String(report.settings.anomalyFactor)} />
524 </Field>
525 <Field label="And at least, $">
526 <Input name="anomalyFloor" inputMode="decimal" defaultValue={dollarsField(report.settings.anomalyFloorMicros)} />
527 </Field>
528 <div className="sm:col-span-2 lg:col-span-4">
529 {failed?.section === "settings" && (
530 <div className="mb-3">
531 <Notice tone="error">{failed.error}</Notice>
532 </div>
533 )}
534 <Button type="submit">Save guardrails</Button>
535 </div>
536 </form>
537 </Section>
538 </main>
539 );
540}
541
542function ProposalRow({ proposal: p, error }: { proposal: PriceProposal; error: string | null }) {
543 const rise = p.proposedCostMicros > p.currentCostMicros;
544 const statusTone = p.status === "open" ? "warn" : p.status === "rejected" || p.status === "superseded" ? "plain" : "mint";
545 return (
546 <li className="rounded-md border border-line px-4 py-3">
547 <div className="flex flex-wrap items-baseline justify-between gap-2">
548 <div className="min-w-0">
549 <span className="font-medium">{p.title}</span> <span className="font-mono text-xs text-faint">{p.meter}</span>
550 </div>
551 <div className="flex items-center gap-2">
552 {p.suspect && <Badge tone="danger">Far off: look first</Badge>}
553 <Badge tone={statusTone}>{p.status}</Badge>
554 </div>
555 </div>
556 <p className="tabular mt-1.5 text-sm">
557 Cost {unitDollars(p.currentCostMicros)} → <span className={rise ? "text-warn" : "text-accent"}>{unitDollars(p.proposedCostMicros)}</span> per {p.unit || "unit"}{" "}
558 <span className="text-faint">({percentLabel(p.changePercent, { signed: true })})</span>
559 </p>
560 <p className="mt-1 text-xs text-muted">{p.reason}</p>
561 <p className="mt-1 text-xs text-faint">
562 From the {p.source}, <When at={p.createdAt} time />
563 {p.decidedBy ? ` · ${p.status} by ${p.decidedBy}` : ""}
564 {p.effectiveAt ? (
565 <>
566 {" "}
567 · in force from <When at={p.effectiveAt} />
568 </>
569 ) : null}
570 {p.note ? ` · “${p.note}”` : ""}
571 </p>
572 {p.status === "open" && (
573 <form method="post" action="#proposals" className="mt-3 flex flex-col gap-2 sm:flex-row sm:items-center">
574 <input type="hidden" name="intent" value="decide" />
575 <input type="hidden" name="id" value={p.id} />
576 <Input name="note" placeholder="A note (needed to reject)" className="sm:max-w-sm" />
577 <div className="flex gap-2">
578 <Button type="submit" name="decision" value="approve" variant="lavender">
579 Approve
580 </Button>
581 <Button type="submit" name="decision" value="reject" variant="quiet">
582 Reject
583 </Button>
584 </div>
585 </form>
586 )}
587 {error && (
588 <div className="mt-2">
589 <Notice tone="error">{error}</Notice>
590 </div>
591 )}
592 </li>
593 );
594}
595
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays596function CapMeter({ label, used, cap, hint }: { label: string; used: number; cap: number; hint: ReactNode }) {
597 const percent = capPercent(used, cap);
598 const tone = cap > 0 && used >= cap ? "bg-danger" : percent >= 75 ? "bg-warn" : "bg-merged";
599 return (
600 <div className="rounded-md border border-line px-4 py-3">
601 <div className="flex flex-wrap items-baseline justify-between gap-2">
602 <span className="text-sm font-medium">{label}</span>
603 <span className="tabular text-sm">
604 {usd(used)} <span className="text-faint">of {cap > 0 ? usd(cap) : "no cap"}</span>
605 </span>
606 </div>
607 <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}>
608 <div className={`h-full ${tone}`} style={{ width: `${percent}%` }} />
609 </div>
610 <p className="mt-1.5 text-xs text-muted">{hint}</p>
611 </div>
612 );
613}
614
615/** g1t's own spend: the daily breaker, comped budgets, and this month by what paid. */
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running616/** The button that runs the nightly analysis now, saying so while it runs. */
617function RunButton() {
618 const navigation = useNavigation();
619 const running = navigation.state !== "idle" && navigation.formData?.get("intent") === "run";
620 return (
621 <Form method="post" className="flex items-center gap-3">
622 <input type="hidden" name="intent" value="run" />
623 <Button variant="quiet" type="submit" disabled={running}>
624 {running ? "Running the analysis…" : "Run the analysis now"}
625 </Button>
626 {running && <span className="text-xs text-muted">Reading the bill and reconciling 31 days; about a minute.</span>}
627 </Form>
628 );
629}
630
631/**
632 * Where the money went, as a short statement: usage sold and running g1t
633 * (each against what paid for it), what g1t gave away on purpose (a budget,
634 * never a loss), and who was paid.
635 */
636function Statement({ report, floor, range, proposals }: { report: CostsReport; floor: number; range: number; proposals: number }) {
637 const o = report.overall;
638 // Reports from before the statement have only the totals.
639 const given = o.givenMicros ?? 0;
640 const usageCost = o.usageCostMicros ?? o.costMicros - given;
641 const usageMargin = o.usageMarginMicros ?? o.usageMicros - usageCost;
642 const usagePercent = o.usageMarginPercent !== undefined ? o.usageMarginPercent : null;
643 const running = o.runningCostMicros ?? 0;
644 const unmapped = o.unmappedCostMicros ?? 0;
645 // Cloudflare's subscriptions are not on the usage bill: the estimate, over the range.
646 const subscriptions = Math.round((report.caps.fixedMonthlyMicros * range) / 30);
647 const moneyIn = o.usageMicros + o.plansMicros;
648 const spent = o.costMicros + subscriptions;
649 const net = moneyIn - spent;
650 const givenParts = [
651 ["comped", o.givenCompedMicros ?? 0],
652 ["free use", o.givenFreeMicros ?? 0],
653 ["trial", o.givenTrialMicros ?? 0],
654 ["open-source pool", o.givenPoolMicros ?? 0],
655 ].filter(([, micros]) => (micros as number) > 0) as [string, number][];
656 const rows: { title: string; note: string; in: number | null; cost: number; result: number | null; tone?: "danger" | "warn" | "muted" }[] = [
657 {
658 title: "Usage sold",
659 note: "What workspaces paid for usage, against what that usage cost",
660 in: o.usageMicros,
661 cost: usageCost,
662 result: usageMargin,
663 tone: usageMargin < 0 ? "danger" : undefined,
664 },
665 {
666 title: "Running g1t",
667 note: "Plans, against Workers, D1, KV, Queues and the rest of the platform",
668 in: o.plansMicros,
669 cost: running,
670 result: o.plansMicros - running,
671 tone: o.plansMicros - running < 0 ? "warn" : undefined,
672 },
673 {
674 title: "Cloudflare subscriptions",
675 note: "Fixed, an estimate (CLOUDFLARE_FIXED_MONTHLY_MICROS), not on the usage bill",
676 in: null,
677 cost: subscriptions,
678 result: -subscriptions,
679 tone: "muted",
680 },
681 ...(unmapped > 0
682 ? [{ title: "Not mapped", note: "Billed by Cloudflare, charged for by nothing yet", in: null, cost: unmapped, result: -unmapped, tone: "danger" as const }]
683 : []),
684 {
685 title: "Given away",
686 note: givenParts.length > 0 ? givenParts.map(([why, micros]) => `${why} ${usd(micros)}`).join(" · ") : "Nothing this range",
687 in: null,
688 cost: given,
689 result: null,
690 tone: "muted",
691 },
692 ];
693 const resultClass = (tone?: string) => (tone === "danger" ? "text-danger" : tone === "warn" ? "text-warn" : tone === "muted" ? "text-muted" : "text-fg");
694 return (
695 <>
696 <div className="mt-5 grid grid-cols-2 gap-3 lg:grid-cols-4">
697 <Stat
698 label="Margin on usage sold"
699 value={percentLabel(usagePercent)}
700 hint={`${usd(o.usageMicros)} paid for usage that cost ${usd(usageCost)}`}
701 tone={marginTone(usagePercent, floor)}
702 />
703 <Stat
704 label="Given away on purpose"
705 value={usd(given)}
706 hint={givenParts.length > 0 ? givenParts.map(([why, micros]) => `${usd(micros)} ${why}`).join(", ") : "Nothing given this range"}
707 />
708 <Stat
709 label="Who g1t paid"
710 value={usd(o.costMicros)}
711 hint={`Cloudflare ${usd(o.cloudflareCostMicros ?? o.costMicros - (o.modelsCostMicros ?? 0))}, model providers ${usd(o.modelsCostMicros ?? 0)}`}
712 />
713 <Stat
714 label="Proposals waiting"
715 value={String(proposals)}
716 hint={report.fetchedAt ? <>Bill read <When at={report.fetchedAt} time /></> : "The bill has not been read yet"}
717 tone={proposals > 0 ? "warn" : undefined}
718 />
719 </div>
720 <div className="mt-3 overflow-x-auto rounded-lg border border-line bg-surface">
721 <table className="w-full min-w-[36rem] text-sm">
722 <thead>
723 <tr className="border-b border-line text-left text-xs text-muted">
724 <th className="px-4 py-2 font-medium sm:px-5">
725 {report.since} to {report.until}
726 </th>
727 <th className="px-4 py-2 text-right font-medium">Paid in</th>
728 <th className="px-4 py-2 text-right font-medium">Cost</th>
729 <th className="px-4 py-2 text-right font-medium sm:pr-5">Result</th>
730 </tr>
731 </thead>
732 <tbody>
733 {rows.map((row) => (
734 <tr key={row.title} className="border-b border-line">
735 <td className="px-4 py-2.5 sm:px-5">
736 <span className="text-fg">{row.title}</span>
737 <span className="block text-xs text-faint">{row.note}</span>
738 </td>
739 <td className="tabular px-4 py-2.5 text-right">{row.in == null ? <span className="text-faint">—</span> : usd(row.in)}</td>
740 <td className="tabular px-4 py-2.5 text-right">{usd(row.cost)}</td>
741 <td className={`tabular px-4 py-2.5 text-right sm:pr-5 ${resultClass(row.tone)}`}>
742 {row.result == null ? "a budget" : usd(row.result, { signed: true })}
743 </td>
744 </tr>
745 ))}
746 <tr>
747 <td className="px-4 py-2.5 sm:px-5">
748 <span className="font-medium text-fg">All in</span>
749 <span className="block text-xs text-faint">
750 {given > 0 ? `${usd(given)} of the cost was given away on purpose; without it, ${usd(net + given, { signed: true })}` : "Everything above"}
751 </span>
752 </td>
753 <td className="tabular px-4 py-2.5 text-right font-medium">{usd(moneyIn)}</td>
754 <td className="tabular px-4 py-2.5 text-right font-medium">{usd(spent)}</td>
755 <td className="tabular px-4 py-2.5 text-right font-medium text-fg sm:pr-5">{usd(net, { signed: true })}</td>
756 </tr>
757 </tbody>
758 </table>
759 </div>
760 </>
761 );
762}
763
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays764function SpendSection({ caps, error }: { caps: CostsReport["caps"]; error: string | null }) {
765 const { rows, totalMicros } = spendRows(caps);
766 const net = caps.revenueMicros - totalMicros;
767 return (
768 <Section
769 className="mt-6"
770 id="spend"
771 title="g1t's own spend"
772 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."
773 >
774 <div className="grid gap-3 lg:grid-cols-2">
775 <CapMeter
776 label={`Today, ${caps.day} (UTC)`}
777 used={caps.todayMicros}
778 cap={caps.dailyCapMicros}
779 hint={
780 caps.tripped ? (
781 <span className="text-danger">
782 Breaker open{caps.trippedAt ? <> since <When at={caps.trippedAt} time /></> : null}: new hosted-model runs g1t pays for wait until 00:00 UTC.
783 </span>
784 ) : caps.liftedBy ? (
785 <>
786 Lifted for today by {caps.liftedBy}
787 {caps.liftNote ? `: “${caps.liftNote}”` : ""}.
788 </>
789 ) : (
790 "Paying workspaces on a live card are never paused. PLATFORM_DAILY_SPEND_CAP_MICROS."
791 )
792 }
793 />
794 {caps.comped.map((b) => (
795 <CapMeter
796 key={b.account}
797 label={`${b.name}, ${caps.month} (comped)`}
798 used={b.usedMicros}
799 cap={b.ceilingMicros}
800 hint={
801 <>
802 {b.ceilingMicros > 0 && b.usedMicros >= b.ceilingMicros ? (
803 <span className="text-danger">Used up: new work on it is refused. </span>
804 ) : null}
805 {b.defaultCeiling ? "The default budget (COMPED_MONTHLY_CEILING_MICROS)" : "Its own budget, in its terms"}. Raise it on{" "}
806 <Link to={`/workspaces/${encodeURIComponent(b.name)}#billing`} className="underline underline-offset-2">
807 its account
808 </Link>
809 : Terms, Limit.
810 </>
811 }
812 />
813 ))}
814 </div>
815 {(caps.tripped || error) && (
816 <form method="post" action="#spend" className="mt-4 flex flex-col gap-2 sm:flex-row sm:items-end">
817 <input type="hidden" name="intent" value="lift" />
818 <Field label="Why lift it" hint="Recorded in the audit log.">
819 <Input name="note" required minLength={5} maxLength={500} placeholder="e.g. Launch day; watching it" className="sm:min-w-[24rem]" />
820 </Field>
821 <Button type="submit" variant="danger">
822 Lift for today
823 </Button>
824 </form>
825 )}
826 {error && (
827 <div className="mt-3">
828 <Notice tone="error">{error}</Notice>
829 </div>
830 )}
831 <div className="-mx-4 mt-5 overflow-x-auto sm:-mx-5">
832 <table className="w-full min-w-[34rem] text-sm">
833 <thead>
834 <tr className="border-b border-line text-left text-xs text-muted">
835 <th className="px-4 py-2 font-medium sm:px-5">{caps.month}, so far</th>
836 <th className="px-4 py-2 text-right font-medium sm:pr-5">g1t paid</th>
837 </tr>
838 </thead>
839 <tbody>
840 {rows.map((row) => (
841 <tr key={row.key} className="border-b border-line align-top">
842 <td className="px-4 py-2.5 sm:px-5">
843 {row.title}
844 <span className="block text-xs text-faint">{row.note}</span>
845 </td>
846 <td className="tabular px-4 py-2.5 text-right sm:pr-5">{usd(row.micros)}</td>
847 </tr>
848 ))}
849 <tr className="border-b border-line font-medium">
850 <td className="px-4 py-2.5 sm:px-5">All of it</td>
851 <td className="tabular px-4 py-2.5 text-right sm:pr-5">{usd(totalMicros)}</td>
852 </tr>
853 <tr className="border-b border-line">
854 <td className="px-4 py-2.5 sm:px-5">
855 Money in
856 <span className="block text-xs text-faint">Usage paid for and the plan, reconciled through yesterday</span>
857 </td>
858 <td className="tabular px-4 py-2.5 text-right sm:pr-5">{usd(caps.revenueMicros)}</td>
859 </tr>
860 <tr>
861 <td className="px-4 py-2.5 sm:px-5">Money in less what g1t paid</td>
862 <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>
863 </tr>
864 </tbody>
865 </table>
866 </div>
867 </Section>
868 );
869}
870
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily871function MappingsTable({ report }: { report: CostsReport }) {
872 return (
873 <div className="-mx-4 overflow-x-auto sm:-mx-5">
874 <table className="w-full min-w-[44rem] text-sm">
875 <thead>
876 <tr className="border-b border-line text-left text-xs text-muted">
877 <th className="px-4 py-2 font-medium sm:px-5">Cloudflare</th>
878 <th className="px-4 py-2 font-medium">g1t product</th>
879 <th className="px-4 py-2 font-medium">Price meter</th>
880 <th className="px-4 py-2 font-medium">Own meter</th>
881 <th className="px-4 py-2 text-right font-medium sm:pr-5">Drift at</th>
882 </tr>
883 </thead>
884 <tbody>
885 {report.mappings.map((m) => (
886 <tr key={`${m.product}-${m.meter}`} className="border-b border-line align-top last:border-0">
887 <td className="px-4 py-2 sm:px-5">
888 <span className="font-mono text-xs">
889 {m.product} / {m.meter}
890 </span>
891 {m.note && <span className="block text-xs text-faint">{m.note}</span>}
892 </td>
893 <td className="px-4 py-2 text-fg-soft">{m.bucket}</td>
894 <td className="px-4 py-2 font-mono text-xs text-muted">{m.priceMeter ?? "—"}</td>
895 <td className="px-4 py-2 font-mono text-xs text-muted">
896 {m.ownMeter ?? "—"}
897 {m.scaleToOwn && <span className="block font-sans text-faint">scaled</span>}
898 </td>
899 <td className="tabular px-4 py-2 text-right text-muted sm:pr-5">{m.driftPercent}%</td>
900 </tr>
901 ))}
902 </tbody>
903 </table>
904 </div>
905 );
906}