flagon-io/g1t

public

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

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

321 lines14,738 bytesCodeBlame
1import { Activity, AlertOctagon, ArrowRight, CalendarClock, CreditCard, FileText, Gauge, HandHelping, TrendingUp } from "lucide-react";
2import type { ReactNode } from "react";
3import { Link, redirect } from "react-router";
4
5import type { Route } from "./+types/overview";
6import { KindBreakdown, MonthsChart } from "~/components/charts";
7import { SignalBadge, StaffName, StageBadge } from "~/components/sales";
8import { Avatar, Notice, PageHeader, Section, Stat } from "~/components/ui";
9import { change, monthLong } from "~/lib/chart";
10import { givenLabel, givenTotal, moneyApart } from "~/lib/pricing";
11import { usd } from "~/lib/money";
12import { admin } from "~/lib/services.server";
13import { settle } from "~/lib/settle";
14import { bySignalUrgency, reachOutHref } from "~/lib/signals";
15import { requireStaff } from "~/lib/staff";
16
17export const meta: Route.MetaFunction = () => [{ title: "Overview · sudo" }, { name: "robots", content: "noindex, nofollow" }];
18
19export async function loader({ request, context }: Route.LoaderArgs) {
20 const staff = requireStaff(context);
21 // The workspaces list used to live here: old links with a search go on to it.
22 const { search, searchParams } = new URL(request.url);
23 if (searchParams.has("q") || searchParams.has("show") || searchParams.has("page")) throw redirect(`/workspaces${search}`);
24
25 const [overview, signals] = await Promise.all([settle(admin.overview()), settle(admin.signals())]);
26 return {
27 me: staff.email,
28 overview: overview.ok ? overview.value : null,
29 overviewError: overview.ok ? null : overview.error,
30 signals: signals.ok ? bySignalUrgency(signals.value) : [],
31 signalCount: signals.ok ? signals.value.length : 0,
32 signalsError: signals.ok ? null : signals.error,
33 };
34}
35
36export default function Overview({ loaderData }: Route.ComponentProps) {
37 const { me, overview, overviewError, signals, signalCount, signalsError } = loaderData;
38 const months = overview?.months ?? [];
39 const current = months.find((month) => month.month === overview?.month) ?? months.at(-1) ?? null;
40 const previous = current ? months[months.indexOf(current) - 1] ?? null : null;
41 const charged = current?.chargedMicros ?? overview?.byKind.reduce((sum, row) => sum + row.chargedMicros, 0) ?? 0;
42 const cost = current?.costMicros ?? overview?.byKind.reduce((sum, row) => sum + row.costMicros, 0) ?? 0;
43 const plans = current?.plansMicros ?? 0;
44 const given = overview?.given ?? [];
45 const givenSum = givenTotal(given);
46 const money = moneyApart({ chargedMicros: charged, plansMicros: plans, costMicros: cost }, givenSum.costMicros);
47 const growth = previous ? change(previous.chargedMicros + (previous.plansMicros ?? 0), money.revenueMicros) : null;
48
49 return (
50 <main className="mx-auto max-w-6xl px-4 py-8 sm:py-10">
51 <PageHeader
52 title="Overview"
53 description={overview ? <>The business in {monthLong(overview.month)}, so far, and who needs a word.</> : "The business at a glance."}
54 />
55
56 {overviewError && (
57 <div className="mt-6">
58 <Notice tone="warn">Billing did not answer for the overview: {overviewError}</Notice>
59 </div>
60 )}
61
62 {overview && (
63 <>
64 <div className="mt-6 grid grid-cols-2 gap-3 lg:grid-cols-4">
65 <Stat
66 label="Revenue this month"
67 value={usd(money.revenueMicros)}
68 hint={
69 <>
70 {usd(charged)} usage, {usd(plans)} plans
71 {growth == null ? "" : ` · ${growth >= 0 ? "+" : "−"}${Math.abs(growth)}% on last month`}
72 </>
73 }
74 tone={growth != null && growth > 0 ? "mint" : undefined}
75 />
76 <Stat
77 label="Margin on what was sold"
78 value={usd(money.marginMicros)}
79 hint={
80 money.marginPercent == null
81 ? "Nothing sold yet"
82 : `${money.marginPercent}% of revenue, after ${usd(money.soldCostMicros)} of cost`
83 }
84 tone={money.marginMicros < 0 ? "danger" : undefined}
85 />
86 <Stat
87 label="Given by g1t"
88 value={usd(givenSum.micros)}
89 hint={`At price; it cost g1t ${usd(givenSum.costMicros)}. Not a loss on sales.`}
90 />
91 <Stat
92 label="Plans"
93 value={String(overview.activePlans ?? 0)}
94 hint={`${usd(overview.planMrrMicros ?? 0)} a month · ${overview.payingWorkspaces} paying this month`}
95 />
96 </div>
97 <p className="mt-2 text-xs text-faint">
98 Cost to g1t in all {usd(cost)}; revenue less all of it, given included: {usd(money.netMicros)}.
99 </p>
100
101 {overview.pools && (
102 <div className="mt-3 grid grid-cols-2 gap-3">
103 <Stat
104 label="Open-source pool"
105 value={`${usd(overview.pools.ossUsedMicros)} of ${usd(overview.pools.ossPoolMicros)}`}
106 hint={`Paid for work on public repositories this month; up to ${usd(overview.pools.ossRepoMicros)} each`}
107 tone={overview.pools.ossUsedMicros >= overview.pools.ossPoolMicros ? "warn" : undefined}
108 />
109 <Stat
110 label="Trials given"
111 value={`${usd(overview.pools.trialGrantedMicros)} of ${usd(overview.pools.trialPoolMicros)}`}
112 hint={`${overview.pools.trialGrants} new workspace${overview.pools.trialGrants === 1 ? "" : "s"} this month; new trials wait when it is given out`}
113 tone={overview.pools.trialGrantedMicros >= overview.pools.trialPoolMicros ? "warn" : undefined}
114 />
115 </div>
116 )}
117
118 <div className="mt-6 grid gap-6 lg:grid-cols-[minmax(0,3fr)_minmax(0,2fr)]">
119 <Section title="Last six months" description="Charged against what it cost g1t, every workspace together.">
120 <MonthsChart months={months} />
121 </Section>
122 <Section title="This month by kind" description="Where the money comes from, and what each earns.">
123 <KindBreakdown kinds={overview.byKind} />
124 </Section>
125 </div>
126
127 <Section
128 title="Given this month, apart from margin"
129 description="Internal use, trials, the open-source pool, goodwill and what g1t covered: at price, and what it cost."
130 className="mt-6"
131 >
132 {given.length === 0 ? (
133 <p className="text-sm text-muted">Nothing given yet this month.</p>
134 ) : (
135 <div className="-mx-4 -my-4 overflow-x-auto sm:-mx-5 sm:-my-5">
136 <table className="w-full min-w-[26rem] text-sm">
137 <thead>
138 <tr className="border-b border-line text-left text-xs text-muted">
139 <th className="px-4 py-2 font-medium sm:pl-5">Source</th>
140 <th className="px-4 py-2 text-right font-medium">At price</th>
141 <th className="px-4 py-2 text-right font-medium sm:pr-5">Cost to g1t</th>
142 </tr>
143 </thead>
144 <tbody>
145 {given.map((row) => (
146 <tr key={row.source} className="border-b border-line last:border-0">
147 <td className="px-4 py-2 sm:pl-5">{givenLabel(row)}</td>
148 <td className="tabular px-4 py-2 text-right">{usd(row.micros)}</td>
149 <td className="tabular px-4 py-2 text-right text-muted sm:pr-5">{usd(row.costMicros)}</td>
150 </tr>
151 ))}
152 </tbody>
153 </table>
154 </div>
155 )}
156 {(overview.internal ?? []).length > 0 && (
157 <ul className="mt-6 space-y-1 border-t border-line pt-3 text-xs">
158 {(overview.internal ?? []).map((row) => (
159 <li key={row.workspace} className="flex flex-wrap justify-between gap-2">
160 <span>
161 <Link to={`/workspaces/${encodeURIComponent(row.workspace)}`} className="font-mono text-fg-soft hover:underline">
162 {row.workspace}
163 </Link>{" "}
164 <span className="text-faint">{row.reason}</span>
165 </span>
166 <span className="tabular text-muted">
167 {usd(row.costMicros)} cost · {row.entries} entries
168 </span>
169 </li>
170 ))}
171 </ul>
172 )}
173 </Section>
174
175 <h2 className="mt-10 text-lg font-semibold tracking-tight">Needs attention</h2>
176 <div className="mt-3 grid grid-cols-2 gap-3 lg:grid-cols-4">
177 <AttentionCard
178 to="/requests"
179 icon={<HandHelping size={15} />}
180 label="Requests"
181 value={overview.openRequests ?? 0}
182 hint="Waiting for an answer, within one business day"
183 tone={(overview.openRequests ?? 0) > 0 ? "warn" : undefined}
184 />
185 <AttentionCard
186 to="/overages"
187 icon={<TrendingUp size={15} />}
188 label="Overages"
189 value={overview.overages ?? 0}
190 hint="Well past their typical month"
191 tone={(overview.overages ?? 0) > 0 ? "warn" : undefined}
192 />
193 <AttentionCard
194 to="/velocity"
195 icon={<Activity size={15} />}
196 label="Spikes"
197 value={overview.openSpikes ?? 0}
198 hint="Paused, waiting on an owner"
199 tone={(overview.openSpikes ?? 0) > 0 ? "danger" : undefined}
200 />
201 <AttentionCard
202 to={reachOutHref({ kind: "at_limit" })}
203 icon={<AlertOctagon size={15} />}
204 label="Stopped"
205 value={overview.stopped}
206 hint="At their limit; work is stopped"
207 tone={overview.stopped > 0 ? "danger" : undefined}
208 />
209 <AttentionCard
210 to={reachOutHref({ kind: "near_ceiling" })}
211 icon={<Gauge size={15} />}
212 label="Near limit"
213 value={overview.nearCeiling}
214 hint="Past 80% of what is available"
215 tone={overview.nearCeiling > 0 ? "warn" : undefined}
216 />
217 <AttentionCard
218 to={reachOutHref({ kind: "declined" })}
219 icon={<CreditCard size={15} />}
220 label="Declined"
221 value={overview.declined}
222 hint="Card declined or payment disputed"
223 tone={overview.declined > 0 ? "danger" : undefined}
224 />
225 <AttentionCard
226 to="/invoices"
227 icon={<FileText size={15} />}
228 label="Open invoices"
229 value={usd(overview.openInvoicesMicros)}
230 hint="Sent and not yet paid"
231 />
232 <AttentionCard
233 to={reachOutHref({ due: true })}
234 icon={<CalendarClock size={15} />}
235 label="Follow-ups due"
236 value={overview.followUpsDue}
237 hint="Next steps due today or earlier"
238 tone={overview.followUpsDue > 0 ? "warn" : undefined}
239 />
240 </div>
241 </>
242 )}
243
244 <Section
245 title="Reach out"
246 description={signalsError ? undefined : signalCount === 0 ? "Nobody needs a word right now." : `The most urgent of ${signalCount}.`}
247 actions={
248 signalCount > 0 && (
249 <Link to="/reach-out" className="inline-flex items-center gap-1 text-sm text-merged hover:underline hover:underline-offset-4">
250 All {signalCount}
251 <ArrowRight size={14} />
252 </Link>
253 )
254 }
255 className="mt-10"
256 >
257 {signalsError ? (
258 <Notice tone="warn">Billing did not answer for signals: {signalsError}</Notice>
259 ) : signals.length === 0 ? (
260 <p className="text-sm text-muted">No signals. Workspaces show up here when they stop, get close to their limit, are declined, or grow.</p>
261 ) : (
262 <ul className="-mx-4 -my-4 divide-y divide-line sm:-mx-5 sm:-my-5">
263 {signals.slice(0, 5).map((signal, index) => (
264 <li key={`${signal.workspace}-${signal.kind}-${index}`}>
265 <Link
266 to={`/workspaces/${encodeURIComponent(signal.workspace)}#sales`}
267 className="flex flex-col gap-2 px-4 py-3 transition-colors hover:bg-raised/40 sm:flex-row sm:items-center sm:gap-4 sm:px-5"
268 >
269 <span className="flex min-w-0 items-center gap-2.5 sm:w-48 sm:shrink-0">
270 <Avatar name={signal.workspace} size={20} />
271 <span className="truncate font-mono text-sm text-fg">{signal.workspace}</span>
272 </span>
273 <span className="flex min-w-0 grow flex-col gap-1 sm:flex-row sm:items-center sm:gap-3">
274 <span className="shrink-0">
275 <SignalBadge kind={signal.kind} />
276 </span>
277 <span className="min-w-0 text-sm text-muted">{signal.detail}</span>
278 </span>
279 <span className="flex shrink-0 items-center gap-3 text-xs sm:w-56 sm:justify-end">
280 <span className="tabular text-fg-soft">{usd(signal.valueMicros)}</span>
281 {signal.stage && signal.stage !== "none" && <StageBadge stage={signal.stage} />}
282 <StaffName email={signal.owner} me={me} />
283 </span>
284 </Link>
285 </li>
286 ))}
287 </ul>
288 )}
289 </Section>
290 </main>
291 );
292}
293
294function AttentionCard({
295 to,
296 icon,
297 label,
298 value,
299 hint,
300 tone,
301}: {
302 to: string;
303 icon: ReactNode;
304 label: string;
305 value: ReactNode;
306 hint: string;
307 tone?: "danger" | "warn";
308}) {
309 const color = tone === "danger" ? "text-danger" : tone === "warn" ? "text-warn" : "text-fg";
310 return (
311 <Link to={to} className="group rounded-lg border border-line bg-surface px-4 py-3 transition-colors hover:border-line-strong">
312 <p className="flex items-center gap-1.5 text-xs text-muted">
313 <span className={tone ? color : "text-faint"}>{icon}</span>
314 {label}
315 <ArrowRight size={12} className="ml-auto text-faint opacity-0 transition-opacity group-hover:opacity-100" />
316 </p>
317 <p className={`tabular mt-1 text-lg font-semibold tracking-tight ${color}`}>{value}</p>
318 <p className="mt-0.5 text-xs text-faint">{hint}</p>
319 </Link>
320 );
321}