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

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