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.

g1t.sh
365 linesCodeBlameRaw
1/**
2 * sudo's navigation, and the roadmap for what g1t is building in house:
3 * its own CRM and back office, in place of bought ones. Each item is a
4 * real page. One marked `soon` renders a placeholder that says what it
5 * will do and why, so staff can see where sudo is going (routes.ts makes
6 * a route for each). No Workers or React imports, so it can be tested
7 * under Node.
8 */
9
10/** The icon a sidebar item shows; components/shell.tsx draws each one. */
11export type NavIcon =
12 | "overview"
13 | "workspaces"
14 | "enterprises"
15 | "reach-out"
16 | "people"
17 | "invoices"
18 | "prices"
19 | "credits"
20 | "usage"
21 | "stripe"
22 | "costs"
23 | "abuse"
24 | "incidents"
25 | "inbox"
26 | "view-as"
27 | "announcements"
28 | "staff"
29 | "audit"
30 | "requests"
31 | "overages"
32 | "velocity"
33 | "invites"
34 | "aliases"
35 // Sections
36 | "customers"
37 | "spend"
38 | "revenue"
39 | "platform"
40 | "support"
41 | "team";
42
43/** What a page that is not built yet says about itself. */
44export type Roadmap = {
45 /** What it will do and why, two to four sentences. */
46 summary: string[];
47 /** What it will have, one line each. */
48 plans: string[];
49 /** Something staff should know now, such as where to go meanwhile. */
50 meanwhile?: { text: string; to?: string; link?: string };
51};
52
53export type NavItem = {
54 label: string;
55 /** Where it lives. A `soon` item's page is the placeholder. */
56 to: string;
57 icon: NavIcon;
58 /** One line, shown on hover. */
59 about: string;
60 /** Other paths under which this item is the current one. */
61 also?: string[];
62 /** Not built yet: the page says what it will be. */
63 soon?: Roadmap;
64 /** A number the sidebar shows beside it while it is above zero, such as requests waiting. */
65 count?: NavCount;
66};
67
68/** The numbers the sidebar can show, read by root.tsx on every page. */
69export type NavCount = "incidents";
70
71export type NavCounts = Partial<Record<NavCount, number>>;
72
73/** What a page or a folded section shows beside it: its items' counts, added up. */
74export function countFor(items: Pick<NavItem, "count">[], counts: NavCounts): number {
75 return items.reduce((sum, item) => sum + (item.count ? (counts[item.count] ?? 0) : 0), 0);
76}
77
78/**
79 * A section of the sidebar: a row with an icon that opens to its pages,
80 * as GitLab and Vercel fold theirs. One with no title is top-level links.
81 */
82export type NavGroup = { title: string | null; icon?: NavIcon; items: NavItem[] };
83
84export const NAV: NavGroup[] = [
85 {
86 title: null,
87 items: [
88 { label: "Overview", to: "/", icon: "overview", about: "The business at a glance: this month, the last six, and who needs a word." },
89 { label: "Reach out", to: "/reach-out", icon: "reach-out", about: "Who is worth a word now, and who on the team has it." },
90 ],
91 },
92 {
93 title: "Customers",
94 icon: "customers",
95 items: [
96 { label: "Workspaces", to: "/workspaces", icon: "workspaces", about: "Every workspace, who owns it, and how it pays." },
97 { label: "Enterprises", to: "/enterprises", icon: "enterprises", about: "Customers paying for several workspaces with one bill." },
98 {
99 label: "Invites",
100 to: "/invites",
101 icon: "invites",
102 about: "Every invitation sent to an address, to find and revoke one.",
103 },
104 {
105 label: "Aliases",
106 to: "/aliases",
107 icon: "aliases",
108 about: "Names that lead to a workspace, such as g1t to flagon-io. Staff set them; customers cannot.",
109 },
110 {
111 label: "People",
112 to: "/people",
113 icon: "people",
114 about: "Everyone with a g1t account, across the workspaces they belong to.",
115 soon: {
116 summary: [
117 "Every person with a g1t account, not just the workspaces they belong to. Support starts from a person far more often than from a workspace: someone writes in from an email address, and staff need to find them, see which workspaces they own or belong to, and what they did lately.",
118 "It gives sales a view of contacts as well as accounts: the owner who signs, the engineer who brought g1t in, and who to ask when a workspace goes quiet.",
119 ],
120 plans: [
121 "Search by username, email or name, across every account",
122 "A person's page: their workspaces and role in each, sign-ins, verified email, two-factor status",
123 "Contacts on a workspace: who is the buyer, who is technical, who to bill",
124 "Merge duplicate accounts, and resend or change a verification email, each recorded",
125 ],
126 meanwhile: { text: "Search a workspace by an owner's username or email on Workspaces.", to: "/workspaces", link: "Workspaces" },
127 },
128 },
129 ],
130 },
131 {
132 title: "Spend",
133 icon: "spend",
134 items: [
135 {
136 label: "Requests",
137 to: "/requests",
138 icon: "requests",
139 about: "Owners asking for a higher limit or help with a month: answered within one business day.",
140 },
141 {
142 label: "Overages",
143 to: "/overages",
144 icon: "overages",
145 about: "Workspaces well past their typical month, what caused it, and goodwill credits.",
146 },
147 {
148 label: "Velocity",
149 to: "/velocity",
150 icon: "velocity",
151 about: "Who is spending fastest right now, against their usual hour, and any spikes.",
152 },
153 ],
154 },
155 {
156 title: "Revenue",
157 icon: "revenue",
158 items: [
159 {
160 label: "Invoices",
161 to: "/invoices",
162 icon: "invoices",
163 about: "Every invoice g1t has sent, to workspaces and enterprises.",
164 },
165 {
166 label: "Plans & prices",
167 to: "/prices",
168 icon: "prices",
169 about: "The price book: what each meter costs g1t and what it sells for.",
170 soon: {
171 summary: [
172 "The price book, edited in sudo instead of in code. Every metered unit (sandbox seconds, builds, app requests and CPU, app-months) has what it costs g1t and what g1t sells it for, and the price follows the cost with a markup.",
173 "Changing a markup or a plan's price is a decision finance makes and should be able to make safely: previewed against last month's usage, with a date it takes effect, and recorded with who made it and why.",
174 ],
175 plans: [
176 "Each meter's cost, markup and price, and where the cost came from (Cloudflare's list price or its bill)",
177 "Edit a markup or a plan's monthly price, with a preview of what last month would have charged",
178 "Schedule a change for the start of a month, so no one is charged mid-month on a new price",
179 "Every change in the history the public pricing page already shows",
180 ],
181 },
182 },
183 {
184 label: "Credits & refunds",
185 to: "/credits",
186 icon: "credits",
187 about: "Every credit staff have given (promotional, goodwill, refunds), what was used, and each month by kind.",
188 },
189 {
190 label: "Usage explorer",
191 to: "/usage",
192 icon: "usage",
193 about: "What customers used, sliced by meter, model, repository and day.",
194 soon: {
195 summary: [
196 "What customers used, across every workspace, sliced any way: by meter, repository and day. It answers the questions that come up in a sales call or a cost review: who uses the most sandbox time, and what a customer spends per pull request.",
197 "Billing records every run and sandbox second with its cost and charge already. This puts it in front of the people who need it, without a query.",
198 ],
199 plans: [
200 "Charged, cost and margin over any range, for everyone or one customer",
201 "Group by meter, model, task, repository or workspace; compare two periods",
202 "The heaviest workspaces this month, and how fast each is growing",
203 "Export to CSV",
204 ],
205 },
206 },
207 ],
208 },
209 {
210 title: "Platform",
211 icon: "platform",
212 items: [
213 { label: "Stripe", to: "/stripe", icon: "stripe", about: "Billing's Stripe keys, webhook, and the events Stripe sent." },
214 {
215 label: "Costs & margin",
216 to: "/costs",
217 icon: "costs",
218 about: "Where g1t's money goes, with what it gave away kept apart; on Bill & pricing, Cloudflare's bill, drift and the price book.",
219 },
220 {
221 label: "Abuse & fraud",
222 to: "/abuse",
223 icon: "abuse",
224 about: "Sandboxes stopped for looking like mining, and disputes and declines.",
225 },
226 {
227 label: "Blocked domains",
228 to: "/blocked-domains",
229 icon: "abuse",
230 about: "Email domains no new account may sign up with.",
231 },
232 {
233 label: "Incidents",
234 to: "/incidents",
235 icon: "incidents",
236 about: "Declare and run incidents, schedule maintenance, and publish postmortems on status.g1t.sh.",
237 count: "incidents",
238 },
239 ],
240 },
241 {
242 title: "Support",
243 icon: "support",
244 items: [
245 {
246 label: "Inbox",
247 to: "/inbox",
248 icon: "inbox",
249 about: "Customers' messages, beside everything about who sent them.",
250 soon: {
251 summary: [
252 "Customers' email and in-app messages in one queue, each beside everything sudo knows about who sent it: their workspaces, what they pay, their limit, their last invoice, and the notes sales kept. Support answers faster when nothing needs looking up.",
253 "Built in house so a conversation can become an action without leaving it: a credit, a billing link, a note on the workspace.",
254 ],
255 plans: [
256 "Assign, snooze and close conversations; see who is answering what",
257 "The sender's workspaces, plan, limit and recent invoices alongside",
258 "Saved replies, and links that open the right page in g1t",
259 "Response times by person and by week",
260 ],
261 },
262 },
263 {
264 label: "View as customer",
265 to: "/view-as",
266 icon: "view-as",
267 about: "See g1t as a customer sees it, audited and time-limited.",
268 soon: {
269 summary: [
270 "See g1t exactly as a customer sees it, to reproduce what they describe. Read-only by default, it is the most sensitive thing staff can do, so it is built to be safe first.",
271 "Every session will be audited and time-limited: it needs a reason, it ends on its own, the customer's own audit log says that g1t staff looked, and nothing can be changed or any secret read while viewing.",
272 ],
273 plans: [
274 "Start a session with a reason (a support conversation or a note); it ends after 30 minutes",
275 "Read-only: no pushes, no settings, no secrets, no billing changes",
276 "A banner on every page while it lasts, and a record in the customer's audit log",
277 "Only roles allowed to, and a list of every session for review",
278 ],
279 },
280 },
281 {
282 label: "Announcements",
283 to: "/announcements",
284 icon: "announcements",
285 about: "Banners and notices for customers: incidents, maintenance, changes.",
286 soon: {
287 summary: [
288 "Tell customers what they need to know, from one place: a banner for an incident or maintenance, a notice about a price change, an email to the owners of the workspaces a change affects.",
289 "Notices can go to everyone or to a slice (owners on an enterprise, workspaces near their limit), with a start and an end, and a record of who sent what.",
290 ],
291 plans: [
292 "Banners in g1t with a start, an end, and a severity",
293 "Email to owners of chosen workspaces, previewed before it goes",
294 "Price-change notices with the notice period the terms require",
295 "A history of everything announced",
296 ],
297 },
298 },
299 ],
300 },
301 {
302 title: "Team",
303 icon: "team",
304 items: [
305 {
306 label: "Staff & roles",
307 to: "/staff",
308 icon: "staff",
309 about: "Who is staff, and what each role may do.",
310 soon: {
311 summary: [
312 "Who is g1t staff and what each of them may do. Today everyone in STAFF_EMAILS may do everything; as the team grows, roles should match the work: sales can see accounts and keep notes but cannot credit, support can make billing links and small credits, finance can change terms and issue refunds.",
313 "Access still decides who gets in. Roles decide what they can do once there, and every refusal is recorded like every change.",
314 ],
315 plans: [
316 "Roles (sales, support, finance, admin) with the permissions each grants",
317 "Per-role limits on money: the most a credit can be without a second person",
318 "Assign roles by email, recorded, with the date each was given",
319 "A second person's approval for comped terms and large credits",
320 ],
321 },
322 },
323 {
324 label: "Audit log",
325 to: "/audit",
326 icon: "audit",
327 about: "Every change made in sudo, by whom, across every customer.",
328 },
329 ],
330 },
331];
332
333/** Whether a section holds the page at `pathname`, so it is drawn open. */
334export function holdsCurrent(group: NavGroup, pathname: string): boolean {
335 return group.items.some((item) => isCurrent(item, pathname));
336}
337
338/** Every item, in sidebar order. */
339export function navItems(nav: NavGroup[] = NAV): NavItem[] {
340 return nav.flatMap((group) => group.items);
341}
342
343/** The items not built yet, each with the placeholder page routes.ts makes. */
344export function soonItems(nav: NavGroup[] = NAV): (NavItem & { soon: Roadmap })[] {
345 return navItems(nav).filter((item): item is NavItem & { soon: Roadmap } => item.soon != null);
346}
347
348/** The item for a path, if one is its page. */
349export function soonFor(pathname: string, nav: NavGroup[] = NAV): (NavItem & { soon: Roadmap }) | null {
350 const path = pathname.replace(/\/+$/, "") || "/";
351 return soonItems(nav).find((item) => item.to === path) ?? null;
352}
353
354/**
355 * Whether `item` is the current page at `pathname`: its own path or one
356 * beneath it. `/` is current only on itself.
357 */
358export function isCurrent(item: Pick<NavItem, "to" | "also">, pathname: string): boolean {
359 const path = pathname.replace(/\/+$/, "") || "/";
360 return [item.to, ...(item.also ?? [])].some((prefix) => {
361 const base = prefix.split("?")[0];
362 if (base === "/") return path === "/";
363 return path === base || path.startsWith(`${base}/`);
364 });
365}