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

919 lines44,720 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: the plan's included usage counts as paid for the usage it covered, out of what plans leave for running g1t; the run button shows it is running with CSS alone (sudo ships no JavaScript)2import { Link, data, redirect } 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: the plan's included usage counts as paid for the usage it covered, out of what plans leave for running g1t; the run button shows it is running with CSS alone (sudo ships no JavaScript)616/**
617 * The button that runs the nightly analysis now. sudo runs no JavaScript,
618 * so app.css's run-button rules say it is running while the page waits.
619 */
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 running620function RunButton() {
621 return (
Costs: the plan's included usage counts as paid for the usage it covered, out of what plans leave for running g1t; the run button shows it is running with CSS alone (sudo ships no JavaScript)622 <form method="post" className="flex items-center gap-3">
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 running623 <input type="hidden" name="intent" value="run" />
Costs: the plan's included usage counts as paid for the usage it covered, out of what plans leave for running g1t; the run button shows it is running with CSS alone (sudo ships no JavaScript)624 <Button variant="quiet" type="submit" className="run-button">
625 <span className="run-idle">Run the analysis now</span>
626 <span className="run-busy">
627 <span className="run-spinner" aria-hidden="true" />
628 Running the analysis…
629 </span>
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 running630 </Button>
Costs: the plan's included usage counts as paid for the usage it covered, out of what plans leave for running g1t; the run button shows it is running with CSS alone (sudo ships no JavaScript)631 <span className="run-note text-xs text-muted">Reading the bill and reconciling 31 days; about a minute.</span>
632 </form>
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 running633 );
634}
635
636/**
637 * Where the money went, as a short statement: usage sold and running g1t
638 * (each against what paid for it), what g1t gave away on purpose (a budget,
639 * never a loss), and who was paid.
640 */
641function Statement({ report, floor, range, proposals }: { report: CostsReport; floor: number; range: number; proposals: number }) {
642 const o = report.overall;
643 // Reports from before the statement have only the totals.
644 const given = o.givenMicros ?? 0;
645 const usageCost = o.usageCostMicros ?? o.costMicros - given;
Costs: the plan's included usage counts as paid for the usage it covered, out of what plans leave for running g1t; the run button shows it is running with CSS alone (sudo ships no JavaScript)646 const included = o.includedMicros ?? 0;
647 const usageIn = o.usageMicros + included;
648 const usageMargin = o.usageMarginMicros ?? usageIn - usageCost;
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 running649 const usagePercent = o.usageMarginPercent !== undefined ? o.usageMarginPercent : null;
650 const running = o.runningCostMicros ?? 0;
651 const unmapped = o.unmappedCostMicros ?? 0;
652 // Cloudflare's subscriptions are not on the usage bill: the estimate, over the range.
653 const subscriptions = Math.round((report.caps.fixedMonthlyMicros * range) / 30);
654 const moneyIn = o.usageMicros + o.plansMicros;
655 const spent = o.costMicros + subscriptions;
656 const net = moneyIn - spent;
657 const givenParts = [
658 ["comped", o.givenCompedMicros ?? 0],
659 ["free use", o.givenFreeMicros ?? 0],
660 ["trial", o.givenTrialMicros ?? 0],
661 ["open-source pool", o.givenPoolMicros ?? 0],
662 ].filter(([, micros]) => (micros as number) > 0) as [string, number][];
663 const rows: { title: string; note: string; in: number | null; cost: number; result: number | null; tone?: "danger" | "warn" | "muted" }[] = [
664 {
665 title: "Usage sold",
Costs: the plan's included usage counts as paid for the usage it covered, out of what plans leave for running g1t; the run button shows it is running with CSS alone (sudo ships no JavaScript)666 note:
667 included > 0
668 ? `What workspaces paid for usage (${usd(o.usageMicros)}) and their plan's included usage (${usd(included)}), against what it cost`
669 : "What workspaces paid for usage, against what that usage cost",
670 in: usageIn,
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 running671 cost: usageCost,
672 result: usageMargin,
673 tone: usageMargin < 0 ? "danger" : undefined,
674 },
675 {
676 title: "Running g1t",
Costs: the plan's included usage counts as paid for the usage it covered, out of what plans leave for running g1t; the run button shows it is running with CSS alone (sudo ships no JavaScript)677 note:
678 included > 0
679 ? `Plans (${usd(o.plansMicros)}) less the included usage they paid for, against Workers, D1, KV, Queues and the rest`
680 : "Plans, against Workers, D1, KV, Queues and the rest of the platform",
681 in: o.plansMicros - included,
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 running682 cost: running,
Costs: the plan's included usage counts as paid for the usage it covered, out of what plans leave for running g1t; the run button shows it is running with CSS alone (sudo ships no JavaScript)683 result: o.plansMicros - included - running,
684 tone: o.plansMicros - included - running < 0 ? "warn" : undefined,
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 running685 },
686 {
687 title: "Cloudflare subscriptions",
688 note: "Fixed, an estimate (CLOUDFLARE_FIXED_MONTHLY_MICROS), not on the usage bill",
689 in: null,
690 cost: subscriptions,
691 result: -subscriptions,
692 tone: "muted",
693 },
694 ...(unmapped > 0
695 ? [{ title: "Not mapped", note: "Billed by Cloudflare, charged for by nothing yet", in: null, cost: unmapped, result: -unmapped, tone: "danger" as const }]
696 : []),
697 {
698 title: "Given away",
699 note: givenParts.length > 0 ? givenParts.map(([why, micros]) => `${why} ${usd(micros)}`).join(" · ") : "Nothing this range",
700 in: null,
701 cost: given,
702 result: null,
703 tone: "muted",
704 },
705 ];
706 const resultClass = (tone?: string) => (tone === "danger" ? "text-danger" : tone === "warn" ? "text-warn" : tone === "muted" ? "text-muted" : "text-fg");
707 return (
708 <>
709 <div className="mt-5 grid grid-cols-2 gap-3 lg:grid-cols-4">
710 <Stat
711 label="Margin on usage sold"
712 value={percentLabel(usagePercent)}
Costs: the plan's included usage counts as paid for the usage it covered, out of what plans leave for running g1t; the run button shows it is running with CSS alone (sudo ships no JavaScript)713 hint={`${usd(usageIn)} paid for usage that cost ${usd(usageCost)}`}
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 running714 tone={marginTone(usagePercent, floor)}
715 />
716 <Stat
717 label="Given away on purpose"
718 value={usd(given)}
719 hint={givenParts.length > 0 ? givenParts.map(([why, micros]) => `${usd(micros)} ${why}`).join(", ") : "Nothing given this range"}
720 />
721 <Stat
722 label="Who g1t paid"
723 value={usd(o.costMicros)}
724 hint={`Cloudflare ${usd(o.cloudflareCostMicros ?? o.costMicros - (o.modelsCostMicros ?? 0))}, model providers ${usd(o.modelsCostMicros ?? 0)}`}
725 />
726 <Stat
727 label="Proposals waiting"
728 value={String(proposals)}
729 hint={report.fetchedAt ? <>Bill read <When at={report.fetchedAt} time /></> : "The bill has not been read yet"}
730 tone={proposals > 0 ? "warn" : undefined}
731 />
732 </div>
733 <div className="mt-3 overflow-x-auto rounded-lg border border-line bg-surface">
734 <table className="w-full min-w-[36rem] text-sm">
735 <thead>
736 <tr className="border-b border-line text-left text-xs text-muted">
737 <th className="px-4 py-2 font-medium sm:px-5">
738 {report.since} to {report.until}
739 </th>
740 <th className="px-4 py-2 text-right font-medium">Paid in</th>
741 <th className="px-4 py-2 text-right font-medium">Cost</th>
742 <th className="px-4 py-2 text-right font-medium sm:pr-5">Result</th>
743 </tr>
744 </thead>
745 <tbody>
746 {rows.map((row) => (
747 <tr key={row.title} className="border-b border-line">
748 <td className="px-4 py-2.5 sm:px-5">
749 <span className="text-fg">{row.title}</span>
750 <span className="block text-xs text-faint">{row.note}</span>
751 </td>
752 <td className="tabular px-4 py-2.5 text-right">{row.in == null ? <span className="text-faint">—</span> : usd(row.in)}</td>
753 <td className="tabular px-4 py-2.5 text-right">{usd(row.cost)}</td>
754 <td className={`tabular px-4 py-2.5 text-right sm:pr-5 ${resultClass(row.tone)}`}>
755 {row.result == null ? "a budget" : usd(row.result, { signed: true })}
756 </td>
757 </tr>
758 ))}
759 <tr>
760 <td className="px-4 py-2.5 sm:px-5">
761 <span className="font-medium text-fg">All in</span>
762 <span className="block text-xs text-faint">
763 {given > 0 ? `${usd(given)} of the cost was given away on purpose; without it, ${usd(net + given, { signed: true })}` : "Everything above"}
764 </span>
765 </td>
766 <td className="tabular px-4 py-2.5 text-right font-medium">{usd(moneyIn)}</td>
767 <td className="tabular px-4 py-2.5 text-right font-medium">{usd(spent)}</td>
768 <td className="tabular px-4 py-2.5 text-right font-medium text-fg sm:pr-5">{usd(net, { signed: true })}</td>
769 </tr>
770 </tbody>
771 </table>
772 </div>
773 </>
774 );
775}
776
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays777function SpendSection({ caps, error }: { caps: CostsReport["caps"]; error: string | null }) {
778 const { rows, totalMicros } = spendRows(caps);
779 const net = caps.revenueMicros - totalMicros;
780 return (
781 <Section
782 className="mt-6"
783 id="spend"
784 title="g1t's own spend"
785 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."
786 >
787 <div className="grid gap-3 lg:grid-cols-2">
788 <CapMeter
789 label={`Today, ${caps.day} (UTC)`}
790 used={caps.todayMicros}
791 cap={caps.dailyCapMicros}
792 hint={
793 caps.tripped ? (
794 <span className="text-danger">
795 Breaker open{caps.trippedAt ? <> since <When at={caps.trippedAt} time /></> : null}: new hosted-model runs g1t pays for wait until 00:00 UTC.
796 </span>
797 ) : caps.liftedBy ? (
798 <>
799 Lifted for today by {caps.liftedBy}
800 {caps.liftNote ? `: “${caps.liftNote}”` : ""}.
801 </>
802 ) : (
803 "Paying workspaces on a live card are never paused. PLATFORM_DAILY_SPEND_CAP_MICROS."
804 )
805 }
806 />
807 {caps.comped.map((b) => (
808 <CapMeter
809 key={b.account}
810 label={`${b.name}, ${caps.month} (comped)`}
811 used={b.usedMicros}
812 cap={b.ceilingMicros}
813 hint={
814 <>
815 {b.ceilingMicros > 0 && b.usedMicros >= b.ceilingMicros ? (
816 <span className="text-danger">Used up: new work on it is refused. </span>
817 ) : null}
818 {b.defaultCeiling ? "The default budget (COMPED_MONTHLY_CEILING_MICROS)" : "Its own budget, in its terms"}. Raise it on{" "}
819 <Link to={`/workspaces/${encodeURIComponent(b.name)}#billing`} className="underline underline-offset-2">
820 its account
821 </Link>
822 : Terms, Limit.
823 </>
824 }
825 />
826 ))}
827 </div>
828 {(caps.tripped || error) && (
829 <form method="post" action="#spend" className="mt-4 flex flex-col gap-2 sm:flex-row sm:items-end">
830 <input type="hidden" name="intent" value="lift" />
831 <Field label="Why lift it" hint="Recorded in the audit log.">
832 <Input name="note" required minLength={5} maxLength={500} placeholder="e.g. Launch day; watching it" className="sm:min-w-[24rem]" />
833 </Field>
834 <Button type="submit" variant="danger">
835 Lift for today
836 </Button>
837 </form>
838 )}
839 {error && (
840 <div className="mt-3">
841 <Notice tone="error">{error}</Notice>
842 </div>
843 )}
844 <div className="-mx-4 mt-5 overflow-x-auto sm:-mx-5">
845 <table className="w-full min-w-[34rem] text-sm">
846 <thead>
847 <tr className="border-b border-line text-left text-xs text-muted">
848 <th className="px-4 py-2 font-medium sm:px-5">{caps.month}, so far</th>
849 <th className="px-4 py-2 text-right font-medium sm:pr-5">g1t paid</th>
850 </tr>
851 </thead>
852 <tbody>
853 {rows.map((row) => (
854 <tr key={row.key} className="border-b border-line align-top">
855 <td className="px-4 py-2.5 sm:px-5">
856 {row.title}
857 <span className="block text-xs text-faint">{row.note}</span>
858 </td>
859 <td className="tabular px-4 py-2.5 text-right sm:pr-5">{usd(row.micros)}</td>
860 </tr>
861 ))}
862 <tr className="border-b border-line font-medium">
863 <td className="px-4 py-2.5 sm:px-5">All of it</td>
864 <td className="tabular px-4 py-2.5 text-right sm:pr-5">{usd(totalMicros)}</td>
865 </tr>
866 <tr className="border-b border-line">
867 <td className="px-4 py-2.5 sm:px-5">
868 Money in
869 <span className="block text-xs text-faint">Usage paid for and the plan, reconciled through yesterday</span>
870 </td>
871 <td className="tabular px-4 py-2.5 text-right sm:pr-5">{usd(caps.revenueMicros)}</td>
872 </tr>
873 <tr>
874 <td className="px-4 py-2.5 sm:px-5">Money in less what g1t paid</td>
875 <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>
876 </tr>
877 </tbody>
878 </table>
879 </div>
880 </Section>
881 );
882}
883
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily884function MappingsTable({ report }: { report: CostsReport }) {
885 return (
886 <div className="-mx-4 overflow-x-auto sm:-mx-5">
887 <table className="w-full min-w-[44rem] text-sm">
888 <thead>
889 <tr className="border-b border-line text-left text-xs text-muted">
890 <th className="px-4 py-2 font-medium sm:px-5">Cloudflare</th>
891 <th className="px-4 py-2 font-medium">g1t product</th>
892 <th className="px-4 py-2 font-medium">Price meter</th>
893 <th className="px-4 py-2 font-medium">Own meter</th>
894 <th className="px-4 py-2 text-right font-medium sm:pr-5">Drift at</th>
895 </tr>
896 </thead>
897 <tbody>
898 {report.mappings.map((m) => (
899 <tr key={`${m.product}-${m.meter}`} className="border-b border-line align-top last:border-0">
900 <td className="px-4 py-2 sm:px-5">
901 <span className="font-mono text-xs">
902 {m.product} / {m.meter}
903 </span>
904 {m.note && <span className="block text-xs text-faint">{m.note}</span>}
905 </td>
906 <td className="px-4 py-2 text-fg-soft">{m.bucket}</td>
907 <td className="px-4 py-2 font-mono text-xs text-muted">{m.priceMeter ?? "—"}</td>
908 <td className="px-4 py-2 font-mono text-xs text-muted">
909 {m.ownMeter ?? "—"}
910 {m.scaleToOwn && <span className="block font-sans text-faint">scaled</span>}
911 </td>
912 <td className="tabular px-4 py-2 text-right text-muted sm:pr-5">{m.driftPercent}%</td>
913 </tr>
914 ))}
915 </tbody>
916 </table>
917 </div>
918 );
919}