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

794 lines39,068 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";
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2import { Link, data, redirect } from "react-router";
3
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;
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it146 // What g1t gave away on purpose (comped workspaces, free periods, the
147 // trial, the pools) is a budget, watched under g1t's own spend; the
148 // margin is measured on what was sold. Older reports lack the fields.
149 const given = report.overall.givenMicros ?? 0;
150 const soldMicros = report.overall.soldMarginMicros ?? report.overall.marginMicros;
151 const soldPercent = report.overall.soldMarginPercent !== undefined ? report.overall.soldMarginPercent : report.overall.marginPercent;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily152 return (
153 <main className="mx-auto max-w-6xl px-4 py-8 sm:py-10">
154 <PageHeader
155 title="Costs & margin"
156 description="What Cloudflare charged g1t, day by day, against what g1t charged for the same things. 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."
157 actions={
158 <form method="post">
159 <input type="hidden" name="intent" value="run" />
160 <Button variant="quiet" type="submit">
sudo: the costs run button is named for what it does, the whole nightly analysis161 Run the analysis now
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily162 </Button>
163 </form>
164 }
165 />
166 <nav aria-label="Range" className="mt-5 flex flex-wrap gap-2">
167 {RANGES.map((days) => (
168 <Link key={days} to={href(days, bucket)} aria-current={days === range ? "page" : undefined} className={chip(days === range)}>
169 Last {days} days
170 </Link>
171 ))}
172 </nav>
173 <div className="mt-5 space-y-3 empty:hidden">
174 {done && <Notice tone="ok">{done}</Notice>}
175 {failed?.section === "run" && <Notice tone="error">{failed.error}</Notice>}
176 {!report.configured && (
177 <Notice tone="warn">
178 Billing cannot read Cloudflare's bill: set the <code className="font-mono">CLOUDFLARE_BILLING_TOKEN</code> secret on g1t-billing (Account:
179 Billing Read and Account Analytics Read). Until then the figures are g1t's own, and Cloudflare's cost shows as nothing.
180 </Notice>
181 )}
182 {banner.map((alert) => (
183 <Notice key={alert.id} tone="error">
184 {alert.detail} <span className="text-faint">Since {alert.since}.</span>
185 </Notice>
186 ))}
187 {elsewhere > 0 && (
188 <p className="text-xs text-muted">
189 {elsewhere} more open: {" "}
190 <a href="#drift" className="underline underline-offset-2">
191 drift and leaks
192 </a>{" "}
193 below, and workspaces that cost more than they pay on{" "}
194 <Link to="/reach-out?kind=cost_over_revenue" className="underline underline-offset-2">
195 Reach out
196 </Link>
197 .
198 </p>
199 )}
200 </div>
201
202 <div className="mt-5 grid grid-cols-2 gap-3 lg:grid-cols-4">
203 <Stat
204 label="Money in"
205 value={usd(report.overall.usageMicros + report.overall.plansMicros)}
206 hint={`${usd(report.overall.usageMicros)} usage, ${usd(report.overall.plansMicros)} plans`}
207 />
208 <Stat
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it209 label="Cost"
210 value={usd(report.overall.costMicros)}
211 hint={given > 0 ? `${usd(given)} of it given away` : `${report.since} to ${report.until}`}
212 />
213 <Stat
214 label="Margin on what was sold"
215 value={percentLabel(soldPercent)}
216 hint={given > 0 ? `${usd(soldMicros)}; ${percentLabel(report.overall.marginPercent)} with what was given` : usd(soldMicros)}
217 tone={marginTone(soldPercent, floor)}
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily218 />
219 <Stat
220 label="Proposals waiting"
221 value={String(open.length)}
222 hint={report.fetchedAt ? <>Bill read <When at={report.fetchedAt} time /></> : "The bill has not been read yet"}
223 tone={open.length > 0 ? "warn" : undefined}
224 />
225 </div>
226
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays227 <SpendSection caps={report.caps} error={failed?.section === "lift" ? failed.error : null} />
228
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily229 <Section
230 className="mt-6"
231 title={product ? `${product.title}, by day` : "By day"}
232 description={
233 product
234 ? "What customers were charged for it at price, before included usage and pools paid part, against what it cost."
235 : "Money in (usage paid for and the plan) against every cost. Pick a product to see its own."
236 }
237 >
238 <nav aria-label="Product" className="mb-4 flex flex-wrap gap-2">
239 <Link to={href(range, null)} aria-current={!bucket ? "page" : undefined} className={chip(!bucket)}>
240 All of g1t
241 </Link>
242 {report.products.map((p) => (
243 <Link key={p.bucket} to={href(range, p.bucket)} aria-current={p.bucket === bucket ? "page" : undefined} className={chip(p.bucket === bucket)}>
244 {p.title}
245 </Link>
246 ))}
247 </nav>
248 <DaysChart
249 days={series}
250 floor={floor}
251 revenueLabel={product ? "Charged at price" : "Money in"}
252 label={product ? `${product.title}: charged against Cloudflare cost by day` : "Money in against Cloudflare cost by day"}
253 />
254 </Section>
255
256 <Section className="mt-6" title="By product" description={`Each of g1t's products over the last ${range} days. The floor is ${floor}%.`}>
257 <div className="-mx-4 overflow-x-auto sm:-mx-5">
258 <table className="w-full min-w-[44rem] text-sm">
259 <thead>
260 <tr className="border-b border-line text-left text-xs text-muted">
261 <th className="px-4 py-2 font-medium sm:px-5">Product</th>
262 <th className="px-4 py-2 text-right font-medium">Charged at price</th>
263 <th className="px-4 py-2 text-right font-medium">Cost</th>
264 <th className="px-4 py-2 text-right font-medium">Price book said</th>
265 <th className="px-4 py-2 text-right font-medium">Margin</th>
266 <th className="px-4 py-2 text-right font-medium sm:pr-5">%</th>
267 </tr>
268 </thead>
269 <tbody>
270 {report.products.map((p) => {
271 const tone = p.overhead ? undefined : marginTone(p.marginPercent, floor);
272 return (
273 <tr key={p.bucket} className="border-b border-line last:border-0">
274 <td className="px-4 py-2.5 sm:px-5">
275 <Link to={href(range, p.bucket)} className="text-fg hover:underline">
276 {p.title}
277 </Link>
278 <span className="block text-xs text-faint">
279 {p.overhead ? "Paid by the plan" : p.costSource === "ledger" ? "Cost from the gateway, not Cloudflare" : p.bucket}
280 </span>
281 </td>
282 <td className="tabular px-4 py-2.5 text-right">{usd(p.valueMicros)}</td>
283 <td className="tabular px-4 py-2.5 text-right">{usd(p.costMicros)}</td>
284 <td className="tabular px-4 py-2.5 text-right text-muted">{usd(p.ownCostMicros)}</td>
285 <td className={`tabular px-4 py-2.5 text-right ${p.marginMicros < 0 && !p.overhead ? "text-danger" : ""}`}>{usd(p.marginMicros)}</td>
286 <td className="px-4 py-2.5 text-right sm:pr-5">
287 {p.overhead ? (
288 <span className="text-faint">—</span>
289 ) : (
290 <Badge tone={tone === "mint" ? "mint" : tone === "warn" ? "warn" : tone === "danger" ? "danger" : "plain"}>{percentLabel(p.marginPercent)}</Badge>
291 )}
292 </td>
293 </tr>
294 );
295 })}
296 </tbody>
297 </table>
298 </div>
299 </Section>
300
301 <Section
302 className="mt-6"
303 id="drift"
304 title="Drift"
305 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)."
306 >
307 {report.drift.length === 0 ? (
308 <p className="text-sm text-muted">Nothing has drifted.</p>
309 ) : (
310 <div className="-mx-4 overflow-x-auto sm:-mx-5">
311 <table className="w-full min-w-[44rem] text-sm">
312 <thead>
313 <tr className="border-b border-line text-left text-xs text-muted">
314 <th className="px-4 py-2 font-medium sm:px-5">Product</th>
315 <th className="px-4 py-2 font-medium">Kind</th>
316 <th className="px-4 py-2 text-right font-medium">g1t</th>
317 <th className="px-4 py-2 text-right font-medium">Cloudflare</th>
318 <th className="px-4 py-2 text-right font-medium sm:pr-5">Delta</th>
319 </tr>
320 </thead>
321 <tbody>
322 {report.drift.map((d) => {
323 const money = d.kind !== "count";
324 return (
325 <tr key={`${d.bucket}-${d.kind}`} className="border-b border-line align-top last:border-0">
326 <td className="px-4 py-2.5 sm:px-5">
327 {d.title}
328 <span className="mt-0.5 block max-w-md text-xs text-faint">{d.detail.replace(`${d.title}: `, "")}</span>
329 </td>
330 <td className="px-4 py-2.5">
331 <Badge tone={d.kind === "leak" ? "danger" : "warn"}>{driftLabel(d.kind)}</Badge>
332 </td>
333 <td className="tabular px-4 py-2.5 text-right">{money ? usd(d.ours) : countLabel(d.ours)}</td>
334 <td className="tabular px-4 py-2.5 text-right">{money ? usd(d.cloudflare) : countLabel(d.cloudflare)}</td>
335 <td className="tabular px-4 py-2.5 text-right sm:pr-5">{percentLabel(d.deltaPercent, { signed: true })}</td>
336 </tr>
337 );
338 })}
339 </tbody>
340 </table>
341 </div>
342 )}
343 </Section>
344
345 <Section
346 className="mt-6"
347 id="proposals"
348 title="Price proposals"
349 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.`}
350 >
351 {report.proposals.length === 0 ? (
352 <p className="text-sm text-muted">No proposals yet.</p>
353 ) : (
354 <ul className="space-y-3">
355 {report.proposals.map((p) => (
356 <ProposalRow key={p.id} proposal={p} error={failed?.section === `proposal-${p.id}` ? failed.error : null} />
357 ))}
358 </ul>
359 )}
360 </Section>
361
362 <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.">
363 <div className="-mx-4 overflow-x-auto sm:-mx-5">
364 <table className="w-full min-w-[44rem] text-sm">
365 <thead>
366 <tr className="border-b border-line text-left text-xs text-muted">
367 <th className="px-4 py-2 font-medium sm:px-5">Meter</th>
368 <th className="px-4 py-2 text-right font-medium">Cost</th>
369 <th className="px-4 py-2 text-right font-medium">Price</th>
370 <th className="px-4 py-2 font-medium">From</th>
371 <th className="px-4 py-2 font-medium sm:pr-5">Why</th>
372 </tr>
373 </thead>
374 <tbody>
375 {report.versions.map((v) => (
376 <tr key={v.id} className="border-b border-line align-top last:border-0">
377 <td className="px-4 py-2.5 sm:px-5">
378 <span className="font-mono text-xs">{v.meter}</span> <span className="text-faint">v{v.version}</span>
379 {!v.appliedAt && (
380 <span className="ml-2">
381 <Badge tone="info">Coming</Badge>
382 </span>
383 )}
384 </td>
385 <td className="tabular px-4 py-2.5 text-right">{unitDollars(v.costMicros)}</td>
386 <td className="tabular px-4 py-2.5 text-right">{unitDollars(v.priceMicros)}</td>
387 <td className="px-4 py-2.5 text-xs text-muted">
388 <When at={v.effectiveAt} />
389 </td>
390 <td className="px-4 py-2.5 text-xs text-faint sm:pr-5">
391 {v.reason} · {v.createdBy}
392 </td>
393 </tr>
394 ))}
395 </tbody>
396 </table>
397 </div>
398 </Section>
399
400 <Section
401 className="mt-6"
402 title="Workspaces that cost most"
403 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."
404 >
405 {report.topWorkspaces.length === 0 ? (
406 <p className="text-sm text-muted">No costs shared out yet.</p>
407 ) : (
408 <div className="-mx-4 overflow-x-auto sm:-mx-5">
409 <table className="w-full min-w-[34rem] text-sm">
410 <thead>
411 <tr className="border-b border-line text-left text-xs text-muted">
412 <th className="px-4 py-2 font-medium sm:px-5">Workspace</th>
413 <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 it414 <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 daily415 <th className="px-4 py-2 text-right font-medium">Paid</th>
416 <th className="px-4 py-2 text-right font-medium sm:pr-5">Net</th>
417 </tr>
418 </thead>
419 <tbody>
420 {report.topWorkspaces.map((w) => {
421 const net = w.revenueMicros - w.costMicros;
422 return (
423 <tr key={w.workspace} className="border-b border-line last:border-0">
424 <td className="px-4 py-2.5 sm:px-5">
425 <Link to={`/workspaces/${encodeURIComponent(w.workspace)}#billing`} className="font-mono text-fg hover:underline">
426 {w.workspace}
427 </Link>
428 {w.internal && <span className="ml-2 text-xs text-faint">g1t's own</span>}
429 </td>
430 <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 it431 <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 daily432 <td className="tabular px-4 py-2.5 text-right">{usd(w.revenueMicros)}</td>
433 <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>
434 </tr>
435 );
436 })}
437 </tbody>
438 </table>
439 </div>
440 )}
441 </Section>
442
443 <Section
444 className="mt-6"
445 title="Cloudflare's lines"
446 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."
447 >
448 {report.lines.length === 0 ? (
sudo: the costs run button is named for what it does, the whole nightly analysis449 <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 daily450 ) : (
451 <div className="-mx-4 overflow-x-auto sm:-mx-5">
452 <table className="w-full min-w-[48rem] text-sm">
453 <thead>
454 <tr className="border-b border-line text-left text-xs text-muted">
455 <th className="px-4 py-2 font-medium sm:px-5">Product / meter</th>
456 <th className="px-4 py-2 text-right font-medium">Quantity</th>
457 <th className="px-4 py-2 text-right font-medium">Cost</th>
458 <th className="px-4 py-2 font-medium sm:pr-5">Cost of</th>
459 </tr>
460 </thead>
461 <tbody>
462 {report.lines.map((l) => (
463 <tr key={`${l.source}-${l.product}-${l.meter}`} className="border-b border-line align-top last:border-0">
464 <td className="px-4 py-2.5 sm:px-5">
465 <span className="font-mono text-xs">
466 {l.product} / {l.meter}
467 </span>
468 <span className="block text-xs text-faint">
469 {l.rawName}
470 {l.source === "artifacts_events" ? " (Artifacts' own count)" : ""}
471 </span>
472 </td>
473 <td className="tabular px-4 py-2.5 text-right">
474 {countLabel(l.quantity)} <span className="text-xs text-faint">{l.unit}</span>
475 </td>
476 <td className="tabular px-4 py-2.5 text-right">{usd(l.costMicros)}</td>
477 <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>
478 </tr>
479 ))}
480 </tbody>
481 </table>
482 </div>
483 )}
484 </Section>
485
486 <Section
487 className="mt-6"
488 id="mappings"
489 title="Mappings"
490 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."
491 >
492 <MappingsTable report={report} />
493 <form method="post" action="#mappings" className="mt-5 grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
494 <input type="hidden" name="intent" value="mapping" />
495 <Field label="Cloudflare product">
496 <Input name="product" placeholder="artifacts" defaultValue={failed?.section === "mapping" ? failed.values?.product : ""} required />
497 </Field>
498 <Field label="Meter prefix" hint="Or * for all of it">
499 <Input name="meter" placeholder="*" defaultValue={failed?.section === "mapping" ? failed.values?.meter : ""} />
500 </Field>
501 <Field label="g1t product">
502 <Input name="bucket" placeholder="git" defaultValue={failed?.section === "mapping" ? failed.values?.bucket : ""} />
503 </Field>
504 <Field label="Drift threshold, %">
505 <Input name="driftPercent" inputMode="decimal" placeholder="10" defaultValue={failed?.section === "mapping" ? failed.values?.driftPercent : ""} />
506 </Field>
507 <Field label="Price meter" hint="Optional">
508 <Input name="priceMeter" placeholder="git_operations" defaultValue={failed?.section === "mapping" ? failed.values?.priceMeter : ""} />
509 </Field>
510 <Field label="Own meter" hint="Optional">
511 <Input name="ownMeter" placeholder="git_operations" defaultValue={failed?.section === "mapping" ? failed.values?.ownMeter : ""} />
512 </Field>
513 <Field label="Note">
514 <Input name="note" defaultValue={failed?.section === "mapping" ? failed.values?.note : ""} />
515 </Field>
516 <div className="flex flex-col justify-end gap-2">
517 <label className="flex items-center gap-2 text-sm text-muted">
518 <input type="checkbox" name="scaleToOwn" className="accent-[var(--g1t-merged)]" /> Scale to g1t's count
519 </label>
520 <div className="flex gap-2">
521 <Button type="submit">Save mapping</Button>
522 <Button type="submit" name="remove" value="1" variant="danger">
523 Remove
524 </Button>
525 </div>
526 </div>
527 {failed?.section === "mapping" && (
528 <div className="sm:col-span-2 lg:col-span-4">
529 <Notice tone="error">{failed.error}</Notice>
530 </div>
531 )}
532 </form>
533 </Section>
534
535 <Section className="mt-6" id="guardrails" title="Guardrails" description="What may change prices without a person, and when staff hear about margin.">
536 <form method="post" action="#guardrails" className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
537 <input type="hidden" name="intent" value="settings" />
538 <Field label="Apply small moves on their own">
539 <label className="flex h-[38px] items-center gap-2 text-sm text-fg-soft">
540 <input type="checkbox" name="autoApply" defaultChecked={report.settings.autoApply} className="accent-[var(--g1t-merged)]" /> On
541 </label>
542 </Field>
543 <Field label="Up to, either way, %">
544 <Input name="autoApplyPercent" inputMode="decimal" defaultValue={String(report.settings.autoApplyPercent)} />
545 </Field>
546 <Field label="Notice for a rise, days">
547 <Input name="noticeDays" inputMode="numeric" defaultValue={String(report.settings.noticeDays)} />
548 </Field>
549 <Field label="Margin floor, %">
550 <Input name="marginFloorPercent" inputMode="decimal" defaultValue={String(report.settings.marginFloorPercent)} />
551 </Field>
552 <Field label="Alert after, days in a row">
553 <Input name="alertDays" inputMode="numeric" defaultValue={String(report.settings.alertDays)} />
554 </Field>
555 <Field label="Ignore days costing under, $">
556 <Input name="minDailyCost" inputMode="decimal" defaultValue={dollarsField(report.settings.minDailyCostMicros)} />
557 </Field>
558 <Field label="Flag a workspace costing more than its revenue ×">
559 <Input name="anomalyFactor" inputMode="decimal" defaultValue={String(report.settings.anomalyFactor)} />
560 </Field>
561 <Field label="And at least, $">
562 <Input name="anomalyFloor" inputMode="decimal" defaultValue={dollarsField(report.settings.anomalyFloorMicros)} />
563 </Field>
564 <div className="sm:col-span-2 lg:col-span-4">
565 {failed?.section === "settings" && (
566 <div className="mb-3">
567 <Notice tone="error">{failed.error}</Notice>
568 </div>
569 )}
570 <Button type="submit">Save guardrails</Button>
571 </div>
572 </form>
573 </Section>
574 </main>
575 );
576}
577
578function ProposalRow({ proposal: p, error }: { proposal: PriceProposal; error: string | null }) {
579 const rise = p.proposedCostMicros > p.currentCostMicros;
580 const statusTone = p.status === "open" ? "warn" : p.status === "rejected" || p.status === "superseded" ? "plain" : "mint";
581 return (
582 <li className="rounded-md border border-line px-4 py-3">
583 <div className="flex flex-wrap items-baseline justify-between gap-2">
584 <div className="min-w-0">
585 <span className="font-medium">{p.title}</span> <span className="font-mono text-xs text-faint">{p.meter}</span>
586 </div>
587 <div className="flex items-center gap-2">
588 {p.suspect && <Badge tone="danger">Far off: look first</Badge>}
589 <Badge tone={statusTone}>{p.status}</Badge>
590 </div>
591 </div>
592 <p className="tabular mt-1.5 text-sm">
593 Cost {unitDollars(p.currentCostMicros)} → <span className={rise ? "text-warn" : "text-accent"}>{unitDollars(p.proposedCostMicros)}</span> per {p.unit || "unit"}{" "}
594 <span className="text-faint">({percentLabel(p.changePercent, { signed: true })})</span>
595 </p>
596 <p className="mt-1 text-xs text-muted">{p.reason}</p>
597 <p className="mt-1 text-xs text-faint">
598 From the {p.source}, <When at={p.createdAt} time />
599 {p.decidedBy ? ` · ${p.status} by ${p.decidedBy}` : ""}
600 {p.effectiveAt ? (
601 <>
602 {" "}
603 · in force from <When at={p.effectiveAt} />
604 </>
605 ) : null}
606 {p.note ? ` · “${p.note}”` : ""}
607 </p>
608 {p.status === "open" && (
609 <form method="post" action="#proposals" className="mt-3 flex flex-col gap-2 sm:flex-row sm:items-center">
610 <input type="hidden" name="intent" value="decide" />
611 <input type="hidden" name="id" value={p.id} />
612 <Input name="note" placeholder="A note (needed to reject)" className="sm:max-w-sm" />
613 <div className="flex gap-2">
614 <Button type="submit" name="decision" value="approve" variant="lavender">
615 Approve
616 </Button>
617 <Button type="submit" name="decision" value="reject" variant="quiet">
618 Reject
619 </Button>
620 </div>
621 </form>
622 )}
623 {error && (
624 <div className="mt-2">
625 <Notice tone="error">{error}</Notice>
626 </div>
627 )}
628 </li>
629 );
630}
631
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays632function CapMeter({ label, used, cap, hint }: { label: string; used: number; cap: number; hint: ReactNode }) {
633 const percent = capPercent(used, cap);
634 const tone = cap > 0 && used >= cap ? "bg-danger" : percent >= 75 ? "bg-warn" : "bg-merged";
635 return (
636 <div className="rounded-md border border-line px-4 py-3">
637 <div className="flex flex-wrap items-baseline justify-between gap-2">
638 <span className="text-sm font-medium">{label}</span>
639 <span className="tabular text-sm">
640 {usd(used)} <span className="text-faint">of {cap > 0 ? usd(cap) : "no cap"}</span>
641 </span>
642 </div>
643 <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}>
644 <div className={`h-full ${tone}`} style={{ width: `${percent}%` }} />
645 </div>
646 <p className="mt-1.5 text-xs text-muted">{hint}</p>
647 </div>
648 );
649}
650
651/** g1t's own spend: the daily breaker, comped budgets, and this month by what paid. */
652function SpendSection({ caps, error }: { caps: CostsReport["caps"]; error: string | null }) {
653 const { rows, totalMicros } = spendRows(caps);
654 const net = caps.revenueMicros - totalMicros;
655 return (
656 <Section
657 className="mt-6"
658 id="spend"
659 title="g1t's own spend"
660 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."
661 >
662 <div className="grid gap-3 lg:grid-cols-2">
663 <CapMeter
664 label={`Today, ${caps.day} (UTC)`}
665 used={caps.todayMicros}
666 cap={caps.dailyCapMicros}
667 hint={
668 caps.tripped ? (
669 <span className="text-danger">
670 Breaker open{caps.trippedAt ? <> since <When at={caps.trippedAt} time /></> : null}: new hosted-model runs g1t pays for wait until 00:00 UTC.
671 </span>
672 ) : caps.liftedBy ? (
673 <>
674 Lifted for today by {caps.liftedBy}
675 {caps.liftNote ? `: “${caps.liftNote}”` : ""}.
676 </>
677 ) : (
678 "Paying workspaces on a live card are never paused. PLATFORM_DAILY_SPEND_CAP_MICROS."
679 )
680 }
681 />
682 {caps.comped.map((b) => (
683 <CapMeter
684 key={b.account}
685 label={`${b.name}, ${caps.month} (comped)`}
686 used={b.usedMicros}
687 cap={b.ceilingMicros}
688 hint={
689 <>
690 {b.ceilingMicros > 0 && b.usedMicros >= b.ceilingMicros ? (
691 <span className="text-danger">Used up: new work on it is refused. </span>
692 ) : null}
693 {b.defaultCeiling ? "The default budget (COMPED_MONTHLY_CEILING_MICROS)" : "Its own budget, in its terms"}. Raise it on{" "}
694 <Link to={`/workspaces/${encodeURIComponent(b.name)}#billing`} className="underline underline-offset-2">
695 its account
696 </Link>
697 : Terms, Limit.
698 </>
699 }
700 />
701 ))}
702 </div>
703 {(caps.tripped || error) && (
704 <form method="post" action="#spend" className="mt-4 flex flex-col gap-2 sm:flex-row sm:items-end">
705 <input type="hidden" name="intent" value="lift" />
706 <Field label="Why lift it" hint="Recorded in the audit log.">
707 <Input name="note" required minLength={5} maxLength={500} placeholder="e.g. Launch day; watching it" className="sm:min-w-[24rem]" />
708 </Field>
709 <Button type="submit" variant="danger">
710 Lift for today
711 </Button>
712 </form>
713 )}
714 {error && (
715 <div className="mt-3">
716 <Notice tone="error">{error}</Notice>
717 </div>
718 )}
719 <div className="-mx-4 mt-5 overflow-x-auto sm:-mx-5">
720 <table className="w-full min-w-[34rem] text-sm">
721 <thead>
722 <tr className="border-b border-line text-left text-xs text-muted">
723 <th className="px-4 py-2 font-medium sm:px-5">{caps.month}, so far</th>
724 <th className="px-4 py-2 text-right font-medium sm:pr-5">g1t paid</th>
725 </tr>
726 </thead>
727 <tbody>
728 {rows.map((row) => (
729 <tr key={row.key} className="border-b border-line align-top">
730 <td className="px-4 py-2.5 sm:px-5">
731 {row.title}
732 <span className="block text-xs text-faint">{row.note}</span>
733 </td>
734 <td className="tabular px-4 py-2.5 text-right sm:pr-5">{usd(row.micros)}</td>
735 </tr>
736 ))}
737 <tr className="border-b border-line font-medium">
738 <td className="px-4 py-2.5 sm:px-5">All of it</td>
739 <td className="tabular px-4 py-2.5 text-right sm:pr-5">{usd(totalMicros)}</td>
740 </tr>
741 <tr className="border-b border-line">
742 <td className="px-4 py-2.5 sm:px-5">
743 Money in
744 <span className="block text-xs text-faint">Usage paid for and the plan, reconciled through yesterday</span>
745 </td>
746 <td className="tabular px-4 py-2.5 text-right sm:pr-5">{usd(caps.revenueMicros)}</td>
747 </tr>
748 <tr>
749 <td className="px-4 py-2.5 sm:px-5">Money in less what g1t paid</td>
750 <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>
751 </tr>
752 </tbody>
753 </table>
754 </div>
755 </Section>
756 );
757}
758
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily759function MappingsTable({ report }: { report: CostsReport }) {
760 return (
761 <div className="-mx-4 overflow-x-auto sm:-mx-5">
762 <table className="w-full min-w-[44rem] text-sm">
763 <thead>
764 <tr className="border-b border-line text-left text-xs text-muted">
765 <th className="px-4 py-2 font-medium sm:px-5">Cloudflare</th>
766 <th className="px-4 py-2 font-medium">g1t product</th>
767 <th className="px-4 py-2 font-medium">Price meter</th>
768 <th className="px-4 py-2 font-medium">Own meter</th>
769 <th className="px-4 py-2 text-right font-medium sm:pr-5">Drift at</th>
770 </tr>
771 </thead>
772 <tbody>
773 {report.mappings.map((m) => (
774 <tr key={`${m.product}-${m.meter}`} className="border-b border-line align-top last:border-0">
775 <td className="px-4 py-2 sm:px-5">
776 <span className="font-mono text-xs">
777 {m.product} / {m.meter}
778 </span>
779 {m.note && <span className="block text-xs text-faint">{m.note}</span>}
780 </td>
781 <td className="px-4 py-2 text-fg-soft">{m.bucket}</td>
782 <td className="px-4 py-2 font-mono text-xs text-muted">{m.priceMeter ?? "—"}</td>
783 <td className="px-4 py-2 font-mono text-xs text-muted">
784 {m.ownMeter ?? "—"}
785 {m.scaleToOwn && <span className="block font-sans text-faint">scaled</span>}
786 </td>
787 <td className="tabular px-4 py-2 text-right text-muted sm:pr-5">{m.driftPercent}%</td>
788 </tr>
789 ))}
790 </tbody>
791 </table>
792 </div>
793 );
794}