Skip to content
373 linesCodeBlameRaw
1import { type ReactNode } from "react";
2
3import type { CostsReport, PriceProposal } from "@g1t/contracts";
4
5import type { Route } from "./+types/costs-bill";
6import { CostsHeader } from "~/components/costs-header";
7import { Badge, Button, EmptyState, Field, Input, Notice, Section, When } from "~/components/ui";
8import { countLabel, driftLabel, percentLabel, proposalOutcome, unitDollars, versionCells } from "~/lib/costs";
9import { type CostsActionResult, costsAction, costsLoader } from "~/lib/costs-route.server";
10import { dollarsField, usd } from "~/lib/money";
11
12export const meta: Route.MetaFunction = () => [{ title: "Bill & pricing · sudo" }, { name: "robots", content: "noindex, nofollow" }];
13
14export const loader = ({ request, context }: Route.LoaderArgs) => costsLoader(request, context);
15export const action = ({ request, context }: Route.ActionArgs) => costsAction(request, context);
16
17/**
18 * Cloudflare's bill as read, what does not add up against it, and the
19 * price book: proposals, versions, mappings and the guardrails on them.
20 */
21export default function CostsBill({ loaderData, actionData }: Route.ComponentProps) {
22 const { range, report, error, done } = loaderData;
23 const failed = actionData && "error" in actionData ? (actionData as CostsActionResult) : null;
24 const description =
25 "Cloudflare's bill as read, line by line, what does not add up against g1t's own counts and price book, and the prices themselves: proposals waiting for a decision, every version, the mappings and the guardrails.";
26 if (!report) {
27 return (
28 <main className="mx-auto max-w-6xl px-4 py-8 sm:py-10">
29 <CostsHeader page="bill" range={range} report={null} done={null} runError={null} description={description} />
30 <div className="mt-5">
31 <Notice tone="warn">Billing did not answer for costs: {error}</Notice>
32 </div>
33 </main>
34 );
35 }
36 return (
37 <main className="mx-auto max-w-6xl px-4 py-8 sm:py-10">
38 <CostsHeader page="bill" range={range} report={report} done={done} runError={failed?.section === "run" ? failed.error : null} description={description} />
39
40 <Section
41 className="mt-5"
42 id="drift"
43 title="Drift"
44 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)."
45 >
46 {report.drift.length === 0 ? (
47 <p className="text-sm text-muted">Nothing has drifted.</p>
48 ) : (
49 <div className="-mx-4 overflow-x-auto sm:-mx-5">
50 <table className="w-full min-w-[44rem] text-sm">
51 <thead>
52 <tr className="border-b border-line text-left text-xs text-muted">
53 <th className="px-4 py-2 font-medium sm:px-5">Product</th>
54 <th className="px-4 py-2 font-medium">Kind</th>
55 <th className="px-4 py-2 text-right font-medium">g1t</th>
56 <th className="px-4 py-2 text-right font-medium">Cloudflare</th>
57 <th className="px-4 py-2 text-right font-medium sm:pr-5">Delta</th>
58 </tr>
59 </thead>
60 <tbody>
61 {report.drift.map((d) => {
62 const money = d.kind !== "count";
63 return (
64 <tr key={`${d.bucket}-${d.kind}`} className="border-b border-line align-top last:border-0">
65 <td className="px-4 py-2.5 sm:px-5">
66 {d.title}
67 <span className="mt-0.5 block max-w-md text-xs text-faint">{d.detail.replace(`${d.title}: `, "")}</span>
68 </td>
69 <td className="px-4 py-2.5">
70 <Badge tone={d.kind === "leak" ? "danger" : "warn"}>{driftLabel(d.kind)}</Badge>
71 </td>
72 <td className="tabular px-4 py-2.5 text-right">{money ? usd(d.ours) : countLabel(d.ours)}</td>
73 <td className="tabular px-4 py-2.5 text-right">{money ? usd(d.cloudflare) : countLabel(d.cloudflare)}</td>
74 <td className="tabular px-4 py-2.5 text-right sm:pr-5">{percentLabel(d.deltaPercent, { signed: true })}</td>
75 </tr>
76 );
77 })}
78 </tbody>
79 </table>
80 </div>
81 )}
82 </Section>
83
84 <Section
85 className="mt-6"
86 id="proposals"
87 title="Price proposals"
88 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.`}
89 >
90 {report.proposals.length === 0 ? (
91 <p className="text-sm text-muted">No proposals yet.</p>
92 ) : (
93 <ul className="space-y-3">
94 {report.proposals.map((p) => (
95 <ProposalRow key={p.id} proposal={p} error={failed?.section === `proposal-${p.id}` ? failed.error : null} />
96 ))}
97 </ul>
98 )}
99 </Section>
100
101 <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.">
102 <div className="-mx-4 overflow-x-auto sm:-mx-5">
103 <table className="w-full min-w-[44rem] text-sm">
104 <thead>
105 <tr className="border-b border-line text-left text-xs text-muted">
106 <th className="px-4 py-2 font-medium sm:px-5">Meter</th>
107 <th className="px-4 py-2 text-right font-medium">Cost</th>
108 <th className="px-4 py-2 text-right font-medium">Price</th>
109 <th className="px-4 py-2 font-medium">From</th>
110 <th className="px-4 py-2 font-medium sm:pr-5">Why</th>
111 </tr>
112 </thead>
113 <tbody>
114 {report.versions.map((v) => {
115 const cells = versionCells(v);
116 return (
117 <tr key={v.id} className="border-b border-line align-top last:border-0">
118 <td className="px-4 py-2.5 sm:px-5">
119 <span className="font-mono text-xs">{v.meter}</span> <span className="text-faint">v{v.version}</span>
120 {!v.appliedAt && (
121 <span className="ml-2">
122 <Badge tone="info">Coming</Badge>
123 </span>
124 )}
125 {cells.note && <span className="block text-xs text-faint">{cells.note}</span>}
126 </td>
127 <td className="tabular px-4 py-2.5 text-right">{cells.cost === "—" ? <span className="text-faint">—</span> : cells.cost}</td>
128 <td className="tabular px-4 py-2.5 text-right">{cells.price}</td>
129 <td className="px-4 py-2.5 text-xs text-muted">
130 <When at={v.effectiveAt} time />
131 </td>
132 <td className="px-4 py-2.5 text-xs text-faint sm:pr-5">
133 {v.reason} · {v.createdBy}
134 </td>
135 </tr>
136 );
137 })}
138 </tbody>
139 </table>
140 </div>
141 </Section>
142
143 <Section
144 className="mt-6"
145 title="Cloudflare's lines"
146 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."
147 >
148 {report.lines.length === 0 ? (
149 <EmptyState title="No lines yet">The bill is read once a day at 04:17 UTC, or now with “Run the analysis now”.</EmptyState>
150 ) : (
151 <div className="-mx-4 overflow-x-auto sm:-mx-5">
152 <table className="w-full min-w-[48rem] text-sm">
153 <thead>
154 <tr className="border-b border-line text-left text-xs text-muted">
155 <th className="px-4 py-2 font-medium sm:px-5">Product / meter</th>
156 <th className="px-4 py-2 text-right font-medium">Quantity</th>
157 <th className="px-4 py-2 text-right font-medium">Cost</th>
158 <th className="px-4 py-2 font-medium sm:pr-5">Cost of</th>
159 </tr>
160 </thead>
161 <tbody>
162 {report.lines.map((l) => (
163 <tr key={`${l.source}-${l.product}-${l.meter}`} className="border-b border-line align-top last:border-0">
164 <td className="px-4 py-2.5 sm:px-5">
165 <span className="font-mono text-xs">
166 {l.product} / {l.meter}
167 </span>
168 <span className="block text-xs text-faint">
169 {l.rawName}
170 {l.source === "artifacts_events" ? " (Artifacts' own count)" : ""}
171 </span>
172 </td>
173 <td className="tabular px-4 py-2.5 text-right">
174 {countLabel(l.quantity)} <span className="text-xs text-faint">{l.unit}</span>
175 </td>
176 <td className="tabular px-4 py-2.5 text-right">{usd(l.costMicros)}</td>
177 <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>
178 </tr>
179 ))}
180 </tbody>
181 </table>
182 </div>
183 )}
184 </Section>
185
186 <Section
187 className="mt-6"
188 id="mappings"
189 title="Mappings"
190 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."
191 >
192 <MappingsTable report={report} />
193 <form method="post" action="#mappings" className="mt-5 grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
194 <input type="hidden" name="intent" value="mapping" />
195 <Field label="Cloudflare product">
196 <Input name="product" placeholder="artifacts" defaultValue={failed?.section === "mapping" ? failed.values?.product : ""} required />
197 </Field>
198 <Field label="Meter prefix" hint="Or * for all of it">
199 <Input name="meter" placeholder="*" defaultValue={failed?.section === "mapping" ? failed.values?.meter : ""} />
200 </Field>
201 <Field label="g1t product">
202 <Input name="bucket" placeholder="git" defaultValue={failed?.section === "mapping" ? failed.values?.bucket : ""} />
203 </Field>
204 <Field label="Drift threshold, %">
205 <Input name="driftPercent" inputMode="decimal" placeholder="10" defaultValue={failed?.section === "mapping" ? failed.values?.driftPercent : ""} />
206 </Field>
207 <Field label="Price meter" hint="Optional">
208 <Input name="priceMeter" placeholder="git_operations" defaultValue={failed?.section === "mapping" ? failed.values?.priceMeter : ""} />
209 </Field>
210 <Field label="Own meter" hint="Optional">
211 <Input name="ownMeter" placeholder="git_operations" defaultValue={failed?.section === "mapping" ? failed.values?.ownMeter : ""} />
212 </Field>
213 <Field label="Note">
214 <Input name="note" defaultValue={failed?.section === "mapping" ? failed.values?.note : ""} />
215 </Field>
216 <div className="flex flex-col justify-end gap-2">
217 <label className="flex items-center gap-2 text-sm text-muted">
218 <input type="checkbox" name="scaleToOwn" className="accent-[var(--g1t-accent)]" /> Scale to g1t's count
219 </label>
220 <div className="flex gap-2">
221 <Button type="submit">Save mapping</Button>
222 <Button type="submit" name="remove" value="1" variant="danger">
223 Remove
224 </Button>
225 </div>
226 </div>
227 {failed?.section === "mapping" && (
228 <div className="sm:col-span-2 lg:col-span-4">
229 <Notice tone="error">{failed.error}</Notice>
230 </div>
231 )}
232 </form>
233 </Section>
234
235 <Section className="mt-6" id="guardrails" title="Guardrails" description="What may change prices without a person, and when staff hear about margin.">
236 <form method="post" action="#guardrails" className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
237 <input type="hidden" name="intent" value="settings" />
238 <Field label="Apply small moves on their own">
239 <label className="flex h-[38px] items-center gap-2 text-sm text-fg-soft">
240 <input type="checkbox" name="autoApply" defaultChecked={report.settings.autoApply} className="accent-[var(--g1t-accent)]" /> On
241 </label>
242 </Field>
243 <Field label="Up to, either way, %">
244 <Input name="autoApplyPercent" inputMode="decimal" defaultValue={String(report.settings.autoApplyPercent)} />
245 </Field>
246 <Field label="Notice for a rise, days">
247 <Input name="noticeDays" inputMode="numeric" defaultValue={String(report.settings.noticeDays)} />
248 </Field>
249 <Field label="Margin floor, %">
250 <Input name="marginFloorPercent" inputMode="decimal" defaultValue={String(report.settings.marginFloorPercent)} />
251 </Field>
252 <Field label="Alert after, days in a row">
253 <Input name="alertDays" inputMode="numeric" defaultValue={String(report.settings.alertDays)} />
254 </Field>
255 <Field label="Ignore days costing under, $">
256 <Input name="minDailyCost" inputMode="decimal" defaultValue={dollarsField(report.settings.minDailyCostMicros)} />
257 </Field>
258 <Field label="Flag a workspace costing more than its revenue ×">
259 <Input name="anomalyFactor" inputMode="decimal" defaultValue={String(report.settings.anomalyFactor)} />
260 </Field>
261 <Field label="And at least, $">
262 <Input name="anomalyFloor" inputMode="decimal" defaultValue={dollarsField(report.settings.anomalyFloorMicros)} />
263 </Field>
264 <Field label="Card fee on card payments">
265 <label className="flex h-[38px] items-center gap-2 text-sm text-fg-soft">
266 <input type="checkbox" name="cardFee" defaultChecked={report.settings.cardFee ?? true} className="accent-[var(--g1t-accent)]" /> Its own line; never on invoiced billing or bank transfers
267 </label>
268 </Field>
269 <div className="sm:col-span-2 lg:col-span-4">
270 {failed?.section === "settings" && (
271 <div className="mb-3">
272 <Notice tone="error">{failed.error}</Notice>
273 </div>
274 )}
275 <Button type="submit">Save guardrails</Button>
276 </div>
277 </form>
278 </Section>
279 </main>
280 );
281}
282
283function ProposalRow({ proposal: p, error }: { proposal: PriceProposal; error: string | null }) {
284 const rise = p.proposedCostMicros > p.currentCostMicros;
285 const outcome = proposalOutcome(p);
286 const statusTone = p.status === "open" ? "warn" : p.status === "rejected" || p.status === "superseded" ? "plain" : "mint";
287 return (
288 <li className="rounded-md border border-line px-4 py-3">
289 <div className="flex flex-wrap items-baseline justify-between gap-2">
290 <div className="min-w-0">
291 <span className="font-medium">{p.title}</span> <span className="font-mono text-xs text-faint">{p.meter}</span>
292 </div>
293 <div className="flex items-center gap-2">
294 {p.suspect && <Badge tone="danger">Far off: look first</Badge>}
295 <Badge tone={statusTone}>{p.status}</Badge>
296 </div>
297 </div>
298 <p className="tabular mt-1.5 text-sm">
299 Cost {unitDollars(p.currentCostMicros)} → <span className={rise ? "text-warn" : "text-success"}>{unitDollars(p.proposedCostMicros)}</span> per {p.unit || "unit"}{" "}
300 <span className="text-faint">({percentLabel(p.changePercent, { signed: true })})</span>
301 </p>
302 <p className="mt-1 text-xs text-muted">{p.reason}</p>
303 <p className="mt-1 text-xs text-faint">
304 From the {p.source}, <When at={p.createdAt} time />
305 {outcome ? ` · ${outcome}` : ""}
306 {p.effectiveAt && p.status !== "superseded" ? (
307 <>
308 {" "}
309 · in force from <When at={p.effectiveAt} time />
310 </>
311 ) : null}
312 {p.note ? ` · “${p.note}”` : ""}
313 </p>
314 {p.status === "open" && (
315 <form method="post" action="#proposals" className="mt-3 flex flex-col gap-2 sm:flex-row sm:items-center">
316 <input type="hidden" name="intent" value="decide" />
317 <input type="hidden" name="id" value={p.id} />
318 <Input name="note" placeholder="A note (needed to reject)" className="sm:max-w-sm" />
319 <div className="flex gap-2">
320 <Button type="submit" name="decision" value="approve" variant="lavender">
321 Approve
322 </Button>
323 <Button type="submit" name="decision" value="reject" variant="quiet">
324 Reject
325 </Button>
326 </div>
327 </form>
328 )}
329 {error && (
330 <div className="mt-2">
331 <Notice tone="error">{error}</Notice>
332 </div>
333 )}
334 </li>
335 );
336}
337
338function MappingsTable({ report }: { report: CostsReport }) {
339 return (
340 <div className="-mx-4 overflow-x-auto sm:-mx-5">
341 <table className="w-full min-w-[44rem] text-sm">
342 <thead>
343 <tr className="border-b border-line text-left text-xs text-muted">
344 <th className="px-4 py-2 font-medium sm:px-5">Cloudflare</th>
345 <th className="px-4 py-2 font-medium">g1t product</th>
346 <th className="px-4 py-2 font-medium">Price meter</th>
347 <th className="px-4 py-2 font-medium">Own meter</th>
348 <th className="px-4 py-2 text-right font-medium sm:pr-5">Drift at</th>
349 </tr>
350 </thead>
351 <tbody>
352 {report.mappings.map((m) => (
353 <tr key={`${m.product}-${m.meter}`} className="border-b border-line align-top last:border-0">
354 <td className="px-4 py-2 sm:px-5">
355 <span className="font-mono text-xs">
356 {m.product} / {m.meter}
357 </span>
358 {m.note && <span className="block text-xs text-faint">{m.note}</span>}
359 </td>
360 <td className="px-4 py-2 text-fg-soft">{m.bucket}</td>
361 <td className="px-4 py-2 font-mono text-xs text-muted">{m.priceMeter ?? "—"}</td>
362 <td className="px-4 py-2 font-mono text-xs text-muted">
363 {m.ownMeter ?? "—"}
364 {m.scaleToOwn && <span className="block font-sans text-faint">scaled</span>}
365 </td>
366 <td className="tabular px-4 py-2 text-right text-muted sm:pr-5">{m.driftPercent}%</td>
367 </tr>
368 ))}
369 </tbody>
370 </table>
371 </div>
372 );
373}