Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

291 linesCodeBlameRaw
1import { AlertTriangle, Gift } from "lucide-react";
2import { Link, data, redirect, useLocation } from "react-router";
3
4import type { Overage } from "@g1t/contracts";
5
6import type { Route } from "./+types/overages";
7import { Hidden } from "~/components/billing";
8import { Badge, Button, EmptyState, Field, Input, Notice, PageHeader, Stat, Textarea, When } from "~/components/ui";
9import { fields, parseGoodwill } from "~/lib/forms";
10import { dollarsField, parseDollars, usd } from "~/lib/money";
11import { FORGIVE_COST_MICROS, goodwillChoices, goodwillLine, goodwillWarning, spikeLabel } from "~/lib/pricing";
12import { DONE, doneKey } from "~/lib/review";
13import { admin, priceBook } from "~/lib/services.server";
14import { settle } from "~/lib/settle";
15import { requireStaff } from "~/lib/staff";
16
17export const meta: Route.MetaFunction = () => [{ title: "Overages · sudo" }, { name: "robots", content: "noindex, nofollow" }];
18
19async function capMicros(): Promise<number> {
20 const book = await settle(priceBook());
21 return (book.ok && book.value.free?.overageForgiveCostMicros) || FORGIVE_COST_MICROS;
22}
23
24export async function loader({ request, context }: Route.LoaderArgs) {
25 requireStaff(context);
26 const [queue, cap] = await Promise.all([settle(admin.overages()), capMicros()]);
27 const done = doneKey(request.url);
28 return {
29 queue: queue.ok ? queue.value : [],
30 error: queue.ok ? null : queue.error,
31 cap,
32 done: done ? DONE[done] : null,
33 };
34}
35
36type Review = {
37 workspace: string;
38 amount: string;
39 day: string;
40 reason: string;
41 absorbedMicros: number;
42 overCap: boolean;
43 needsReason: boolean;
44 amountMicros: number;
45};
46
47type ActionResult = { error: string; workspace: string; values: Record<string, string> } | { review: Review };
48
49export async function action({ request, context }: Route.ActionArgs) {
50 const staff = requireStaff(context);
51 const form = await request.formData();
52 const values = fields(form, "workspace", "amount", "reason", "day");
53 const fail = (error: string) => data<ActionResult>({ error, workspace: values.workspace, values }, { status: 422 });
54 const [queue, cap] = await Promise.all([admin.overages(), capMicros()]);
55 // What is credited comes from billing's queue, not the form.
56 const overage = queue.find((row) => row.workspace === values.workspace);
57 if (!overage) return fail("That workspace is no longer in the Overages queue. Give credit from its page instead.");
58 const parsed = parseGoodwill(form, (amount) => goodwillWarning(amount, overage.goodwill, overage.lastGoodwillAt, cap).needsReason);
59 // The amount as typed, even when the form is not complete yet: it decides whether the red review shows.
60 const typed = values.amount ? parseDollars(values.amount) : null;
61 const warning = goodwillWarning(typed, overage.goodwill, overage.lastGoodwillAt, cap);
62 // Past the cap, staff see what g1t absorbs in red, with the reason, before it goes.
63 if (warning.overCap && form.get("confirm") !== "yes") {
64 return {
65 review: {
66 workspace: overage.workspace,
67 amount: values.amount ?? "",
68 day: values.day ?? "",
69 reason: values.reason ?? "",
70 ...warning,
71 amountMicros: typed ?? overage.goodwill.creditMicros,
72 },
73 } satisfies ActionResult;
74 }
75 if (!parsed.ok) return fail(parsed.error);
76 const result = await admin.goodwill(overage.workspace, parsed.value.amountMicros, parsed.value.reason, staff.email, parsed.value.day);
77 if (!result.ok) return fail(result.error.message);
78 throw redirect("/overages?done=goodwill");
79}
80
81export default function Overages({ loaderData, actionData }: Route.ComponentProps) {
82 const { queue, error, cap, done } = loaderData;
83 const result = actionData as ActionResult | undefined;
84 const review = result && "review" in result ? result.review : null;
85 const totals = queue.reduce(
86 (sum, row) => ({
87 overage: sum.overage + row.goodwill.overageMicros,
88 margin: sum.margin + row.goodwill.marginMicros,
89 absorbed: sum.absorbed + (row.goodwillAvailable ? row.goodwill.absorbedMicros : 0),
90 }),
91 { overage: 0, margin: 0, absorbed: 0 },
92 );
93 return (
94 <main className="mx-auto max-w-6xl px-4 py-8 sm:py-10">
95 <PageHeader
96 title="Overages"
97 description={
98 <>
99 Workspaces whose month went well past their usual (twice it, and $10 over), hit a spend spike, or asked for help. A goodwill credit always returns
100 g1t's margin on the overage and absorbs at most {usd(cap)} of real cost in one click, once per workspace in 12 months.
101 </>
102 }
103 />
104 <div className="mt-5 space-y-3 empty:hidden">
105 {done && <Notice tone="ok">{done}</Notice>}
106 {error && <Notice tone="warn">Billing did not answer for overages: {error}</Notice>}
107 </div>
108 {queue.length > 0 && (
109 <div className="mt-5 grid grid-cols-2 gap-3 lg:grid-cols-4">
110 <Stat label="In the queue" value={String(queue.length)} hint={`${queue.filter((row) => row.request).length} asked for help`} />
111 <Stat label="Over their usual" value={usd(totals.overage)} hint="This month, above each typical month" />
112 <Stat label="g1t's margin in it" value={usd(totals.margin)} hint="Returned by any goodwill credit" />
113 <Stat
114 label="Real cost, if all forgiven"
115 value={usd(totals.absorbed)}
116 hint="One-click credits still available"
117 tone={totals.absorbed > 0 ? "warn" : undefined}
118 />
119 </div>
120 )}
121 {review && <ReviewPanel review={review} cap={cap} />}
122 <div className="mt-6 space-y-4">
123 {queue.length === 0 && !error ? (
124 <EmptyState title="No overages this month">Workspaces show up here when a month runs well past their usual, or a spike pauses them.</EmptyState>
125 ) : (
126 queue.map((row) => (
127 <OverageCard key={row.workspace} row={row} cap={cap} error={result && "error" in result && result.workspace === row.workspace ? result : null} />
128 ))
129 )}
130 </div>
131 </main>
132 );
133}
134
135/** Past the cap: what g1t absorbs in red, and the reason, before it is given. */
136function ReviewPanel({ review, cap }: { review: Review; cap: number }) {
137 const { pathname } = useLocation();
138 return (
139 <section id="review" className="mt-6 scroll-mt-20 rounded-lg border border-danger/50 bg-danger/8 p-4 sm:p-5">
140 <h2 className="flex items-center gap-2 font-semibold tracking-tight text-danger">
141 <AlertTriangle size={16} />
142 g1t absorbs {usd(review.absorbedMicros)} of real cost
143 </h2>
144 <p className="mt-2 text-sm text-fg-soft">
145 A {usd(review.amountMicros)} credit to <span className="font-mono">{review.workspace}</span> is past the {usd(cap)} a goodwill credit absorbs in one
146 click. That is money g1t already paid Cloudflare. Say why, for whoever looks next; it goes in the audit log with your email.
147 </p>
148 <form method="post" action={`${pathname}#review`} className="mt-4 space-y-3">
149 <Hidden values={{ workspace: review.workspace, amount: review.amount, day: review.day, confirm: "yes" }} />
150 <Field label="Reason" hint="Required: a sentence at least.">
151 <Textarea name="reason" rows={2} required minLength={10} maxLength={500} defaultValue={review.reason} />
152 </Field>
153 <div className="flex flex-wrap items-center gap-2">
154 <Button type="submit" variant="danger">
155 Give {usd(review.amountMicros)} anyway
156 </Button>
157 <Link to="/overages" className="px-2 text-sm text-muted hover:text-fg">
158 Cancel
159 </Link>
160 </div>
161 </form>
162 </section>
163 );
164}
165
166function OverageCard({ row, cap, error }: { row: Overage; cap: number; error: { error: string; values: Record<string, string> } | null }) {
167 const { pathname } = useLocation();
168 const { goodwill } = row;
169 const spike = row.spike ? spikeLabel(row.spike.status) : null;
170 const choices = goodwillChoices(goodwill).map((choice) => ({
171 ...choice,
172 ...goodwillWarning(choice.micros, goodwill, row.lastGoodwillAt, cap),
173 amount: choice.micros ?? goodwill.creditMicros,
174 }));
175 return (
176 <section id={row.workspace} className="scroll-mt-20 rounded-lg border border-line bg-surface">
177 <header className="flex flex-wrap items-start justify-between gap-3 border-b border-line px-4 py-3 sm:px-5">
178 <div className="min-w-0">
179 <p className="flex flex-wrap items-center gap-2">
180 <Link to={`/workspaces/${encodeURIComponent(row.workspace)}#billing`} className="font-mono font-medium text-fg hover:underline">
181 {row.workspace}
182 </Link>
183 <Badge>{row.plan}</Badge>
184 {spike && <Badge tone={spike.tone === "plain" ? "plain" : spike.tone}>{spike.label}</Badge>}
185 {row.request && <Badge tone="warn">Asked for help</Badge>}
186 {!row.goodwillAvailable && (
187 <Badge tone="danger">
188 Goodwill given <When at={row.lastGoodwillAt} />
189 </Badge>
190 )}
191 </p>
192 <p className="mt-0.5 text-xs text-faint">
193 Typical month {usd(row.typicalMonthMicros)} · this month {usd(row.thisMonthMicros)} · cost to g1t {usd(row.costMicros)}
194 </p>
195 </div>
196 </header>
197 <div className="grid gap-5 p-4 sm:p-5 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
198 <div className="min-w-0 space-y-4">
199 {row.request && (
200 <blockquote className="border-l-2 border-warn/50 pl-3 text-sm wrap-break-word whitespace-pre-line text-fg-soft">
201 “{row.request.reason}” <span className="text-xs text-faint">— {row.request.createdBy}</span>
202 </blockquote>
203 )}
204 <div>
205 <p className="text-xs font-medium text-muted">The goodwill math</p>
206 <dl className="mt-2 space-y-1 text-sm">
207 <Line label="Over the typical month" value={usd(goodwill.overageMicros)} />
208 <Line label="g1t's margin on it, always returned" value={usd(goodwill.marginMicros)} tone="mint" />
209 <Line label="What it cost g1t" value={usd(goodwill.costMicros)} />
210 <Line label={`Real cost absorbed in one click (at most ${usd(cap)})`} value={usd(goodwill.absorbedMicros)} tone="warn" />
211 <Line label="One-click credit" value={usd(goodwill.creditMicros)} strong />
212 </dl>
213 </div>
214 {row.topEntries.length > 0 && (
215 <div>
216 <p className="text-xs font-medium text-muted">What caused it</p>
217 <ul className="mt-2 divide-y divide-line rounded-md border border-line text-xs">
218 {row.topEntries.slice(0, 5).map((entry) => (
219 <li key={entry.id} className="flex items-start justify-between gap-3 px-3 py-2">
220 <span className="min-w-0">
221 <span className="block wrap-break-word text-fg-soft">{entry.description}</span>
222 <span className="font-mono text-faint">
223 {[entry.repo && (entry.number != null ? `${entry.repo}#${entry.number}` : entry.repo), entry.task, entry.model]
224 .filter(Boolean)
225 .join(" · ")}
226 </span>
227 </span>
228 <span className="tabular shrink-0">{usd(-entry.amountMicros)}</span>
229 </li>
230 ))}
231 </ul>
232 </div>
233 )}
234 </div>
235 <div className="min-w-0 space-y-3">
236 <ul className="space-y-2">
237 {choices.map((choice) => (
238 <li key={choice.label} className={`rounded-md border px-3 py-2 text-sm ${choice.overCap ? "border-danger/50 bg-danger/8" : "border-line bg-bg"}`}>
239 <p className="flex justify-between gap-3">
240 <span className="font-medium">{choice.label}</span>
241 <span className="tabular">{usd(choice.amount)}</span>
242 </p>
243 <p className={`mt-0.5 text-xs ${choice.overCap ? "text-danger" : "text-muted"}`}>
244 Returns {usd(Math.min(choice.amount, goodwill.marginMicros))} of margin; g1t absorbs {usd(choice.absorbedMicros)} of real cost
245 {choice.overCap ? `, past the ${usd(cap)} cap` : ""}.{choice.needsReason ? " Needs a reason." : ""}
246 </p>
247 </li>
248 ))}
249 </ul>
250 <form method="post" action={`${pathname}#${row.workspace}`} className="space-y-3">
251 <input type="hidden" name="workspace" value={row.workspace} />
252 {error && <Notice tone="error">{error.error}</Notice>}
253 <div className="grid gap-3 sm:grid-cols-2">
254 <Field label="Amount $" hint="Blank: the one-click credit.">
255 <Input name="amount" inputMode="decimal" placeholder={dollarsField(goodwill.creditMicros)} defaultValue={error?.values.amount ?? ""} />
256 </Field>
257 <Field label="Day of the usage" hint="Blank: the spike's day, or today.">
258 <Input type="date" name="day" defaultValue={error?.values.day ?? row.spike?.detectedAt.slice(0, 10) ?? ""} />
259 </Field>
260 </div>
261 <Field label="Reason" hint={row.goodwillAvailable ? "Needed past the one-click credit." : "Needed: a credit was given in the last 12 months."}>
262 <Textarea
263 name="reason"
264 rows={2}
265 maxLength={500}
266 defaultValue={error?.values.reason ?? ""}
267 placeholder="e.g. A review loop ran overnight on a misconfigured workflow."
268 />
269 </Field>
270 <div className="flex flex-wrap items-center justify-between gap-2">
271 <p className="text-xs text-faint">On their statement as “{goodwillLine("YYYY-MM-DD")}”.</p>
272 <Button type="submit" variant="lavender">
273 <Gift size={14} />
274 Give goodwill credit
275 </Button>
276 </div>
277 </form>
278 </div>
279 </div>
280 </section>
281 );
282}
283
284function Line({ label, value, tone, strong = false }: { label: string; value: string; tone?: "mint" | "warn"; strong?: boolean }) {
285 return (
286 <div className={`flex justify-between gap-4 ${strong ? "border-t border-line pt-1 font-medium" : ""}`}>
287 <dt className="text-muted">{label}</dt>
288 <dd className={`tabular ${tone === "mint" ? "text-success" : tone === "warn" ? "text-warn" : "text-fg"}`}>{value}</dd>
289 </div>
290 );
291}