g1t/apps/web/app/components/spike-banner.tsx

71 lines2,948 bytesCodeBlame
1import { AlertTriangle, OctagonX, Play } from "lucide-react";
2import { Form } from "react-router";
3
4import type { Entitlements } from "@g1t/contracts";
5
6import { dollars } from "../lib/billing";
7import { Button } from "./ui";
8
9/**
10 * Compute paused by a spend spike, with the owners' choice: keep going for
11 * 24 hours, or stop. Also a hold by g1t, which only g1t lifts. Posts to the
12 * workspace's billing page, so it works from anywhere in the app.
13 */
14export function SpikeBanner({
15 slug,
16 entitlements,
17 owner,
18 compact = false,
19}: {
20 slug: string;
21 entitlements: Pick<Entitlements, "paused" | "spike">;
22 owner: boolean;
23 compact?: boolean;
24}) {
25 const spike = entitlements.spike?.status === "open" ? entitlements.spike : null;
26 if (!spike && !entitlements.paused) return null;
27 const text = spike
28 ? `Spending spiked: ${dollars(spike.hourMicros)} in the last hour, against a usual ${dollars(spike.averageMicros)}. New agents, checks and builds wait until an owner decides; work already running finishes.`
29 : `g1t paused new compute for this workspace: ${entitlements.paused}`;
30 return (
31 <div
32 id="spike"
33 role="alert"
34 className={
35 compact
36 ? "flex flex-wrap items-center justify-center gap-x-4 gap-y-2 border-b border-danger/30 bg-danger/10 px-4 py-2 text-sm"
37 : "mb-6 scroll-mt-20 rounded-xl border border-danger/40 bg-danger/5 p-5"
38 }
39 >
40 <p className={compact ? "flex items-center gap-2" : "flex gap-2.5 text-sm"}>
41 <AlertTriangle size={compact ? 15 : 17} className="shrink-0 text-danger" />
42 <span>{compact ? (spike ? `${slug}: spending spiked, so new compute is paused.` : `${slug}: new compute is paused.`) : text}</span>
43 </p>
44 {spike && owner ? (
45 <Form method="post" action={`/${slug}/-/billing`} className={compact ? "flex gap-2" : "mt-4 flex flex-wrap gap-2"}>
46 <input type="hidden" name="intent" value="spike" />
47 <Button variant="accent" type="submit" name="decision" value="keep">
48 <Play size={14} />
49 Keep going
50 </Button>
51 <Button variant="quiet" type="submit" name="decision" value="stop">
52 <OctagonX size={14} />
53 Stop
54 </Button>
55 </Form>
56 ) : spike ? (
57 <p className={compact ? "text-xs text-muted" : "mt-3 text-sm text-muted"}>An owner chooses Keep going or Stop on Billing.</p>
58 ) : (
59 <p className={compact ? "text-xs text-muted" : "mt-3 text-sm text-muted"}>
60 Write to <a href="mailto:hey@flagon.io" className="underline underline-offset-2">hey@flagon.io</a> to have it looked at.
61 </p>
62 )}
63 {!compact && spike && (
64 <p className="mt-3 text-xs text-faint">
65 Keep going lets work start again for 24 hours, unless spending doubles again first. Stop keeps new compute paused until you
66 choose Keep going.
67 </p>
68 )}
69 </div>
70 );
71}